lambdas 排序
与Peter Lawrey合作撰写 。
几天前,我对使用新的Java8声明式的排序性能提出了一个严重的问题。 在这里查看博客文章。 在那篇文章中,我仅指出了问题,但在这篇文章中,我将更深入地了解和解释问题的原因。 这将通过使用声明式样式重现问题,然后一点一点地修改代码来完成,直到我们消除了性能问题并保留了使用旧样式比较所期望的性能。
回顾一下,我们对此类的实例进行排序:
private static class MyComparableInt{private int a,b,c,d;public MyComparableInt(int i) {a = i%2;b = i%10;c = i%1000;d = i;}public int getA() return a;public int getB() return b;public int getC() return c;public int getD() return d;
}
使用声明性的Java 8样式(如下),大约需要6秒钟才能排序10m个实例:
List mySortedList = myComparableList.stream().sorted(Comparator.comparing(MyComparableInt::getA).thenComparing(MyComparableInt::getB).thenComparing(MyComparableInt::getC).thenComparing(MyComparableInt::getD)).collect(Collectors.toList());
使用自定义排序器(如下)需要约1.6秒的时间来排序10m个实例。
这是排序的代码调用:
List mySortedList = myComparableList.stream().sorted(MyComparableIntSorter.INSTANCE).collect(Collectors.toList());
使用此自定义比较器:
public enum MyComparableIntSorter implements Comparator<MyComparableInt>{INSTANCE;@Overridepublic int compare(MyComparableInt o1, MyComparableInt o2) {int comp = Integer.compare(o1.getA(), o2.getA());if(comp==0){comp = Integer.compare(o1.getB(), o2.getB());if(comp==0){comp = Integer.compare(o1.getC(), o2.getC());if(comp==0){comp = Integer.compare(o1.getD(), o2.getD());}}}return comp;}}
让我们在类中创建一个comparing
方法,以便我们可以更紧密地分析代码。 comparing
方法的原因是允许我们轻松交换实现,但调用代码保持不变。
在所有情况下,这都是comparing
方法的调用方式:
List mySortedList = myComparableList.stream().sorted(comparing(MyComparableInt::getA,MyComparableInt::getB,MyComparableInt::getC,MyComparableInt::getD)).collect(Collectors.toList());
比较的第一个实现几乎是jdk中的一个副本。
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> ke1,Function<? super T, ? extends U> ke2,Function<? super T, ? extends U> ke3,Function<? super T, ? extends U> ke4){return Comparator.comparing(ke1).thenComparing(ke2).thenComparing(ke3).thenComparing(ke4);}
毫不奇怪,这花了大约6秒钟才能完成测试-但是至少我们重现了该问题,并为进一步进行奠定了基础。
让我们看一下该测试的飞行记录:
可以看出有两个大问题:
-
lambda$comparing
方法中的性能问题 - 反复调用
Integer.valueOf
(自动装箱)
让我们尝试处理比较方法中的第一个方法。 乍一看,这似乎很奇怪,因为当您查看代码时,该方法中没有发生太多事情。 然而,随着代码找到该函数的正确实现,虚拟表查找将在这里广泛进行。 当从一行代码中调用多种方法时,将使用虚拟表查找。 我们可以通过下面的comparing
实现消除这种延迟源。 通过扩展Function
接口的所有用途,每一行只能调用一个实现,因此可以内联该方法。
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> ke1,Function<? super T, ? extends U> ke2,Function<? super T, ? extends U> ke3,Function<? super T, ? extends U> ke4){return (c1, c2) -> {int comp = compare(ke1.apply(c1), ke1.apply(c2));if (comp == 0) {comp = compare(ke2.apply(c1), ke2.apply(c2));if (comp == 0) {comp = compare(ke3.apply(c1), ke3.apply(c2));if (comp == 0) {comp = compare(ke4.apply(c1), ke4.apply(c2));}}}return comp;};}
通过展开方法,JIT应该能够内联方法查找。
确实,时间几乎减半到3.5秒,让我们看一下此运行的飞行记录:
当我第一次看到此消息时,我感到非常惊讶,因为到目前为止,我们还没有进行任何更改来减少对Integer.valueOf
的调用,但是该百分比已经下降了! 实际上发生的事情是,由于我们进行了允许内联的更改,已对Integer.valueOf
进行了内联,并且将Integer.valueOf
花费的时间归咎于调用程序( lambda$comparing
),后者已对被调用者( Integer.valueOf
)。 这是事件探查器中的一个常见问题,因为他们可能会误解应归咎于哪种方法,尤其是在进行内联时。
但是我们知道在之前的Flight Recording Integer.valueOf
已突出显示,因此让我们通过comparing
实现comparing
删除,看看是否可以进一步减少时间。
return (c1, c2) -> {int comp = compare(ke1.applyAsInt(c1), ke1.applyAsInt(c2));if (comp == 0) {comp = compare(ke2.applyAsInt(c1), ke2.applyAsInt(c2));if (comp == 0) {comp = compare(ke3.applyAsInt(c1), ke3.applyAsInt(c2));if (comp == 0) {comp = compare(ke4.applyAsInt(c1), ke4.applyAsInt(c2));}}}return comp;
};
通过这种实现,时间可以缩短到1.6s,这是我们使用自定义比较器可以实现的。
让我们再次查看此运行的飞行记录:
现在,所有时间都在使用实际的排序方法,而不是开销。
总之,我们从这次调查中学到了一些有趣的事情:
- 由于自动装箱和虚拟表查找的成本,在某些情况下,使用新的Java8声明式排序将比编写自定义比较器慢4倍。
- FlightRecorder虽然比其他分析器要好(有关此问题,请参阅我的第一篇博客文章 ),但仍将时间归因于错误的方法,尤其是在进行内联时。
翻译自: https://www.javacodegeeks.com/2015/01/java8-lambdas-sorting-performance-pitfall-explained.html
lambdas 排序