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;
@Override
public 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 排序