Java 8流:多重过滤器vs.复杂条件

有时候你想用多个条件来过滤Stream:

myList.stream().filter(x -> x.size() > 10).filter(x -> x.isCool()) ...

或者你也可以对复杂条件和 filter做同样的事情:

myList.stream().filter(x -> x.size() > 10 && x -> x.isCool()) ...

我的猜测是第二种方法有更好的性能特征,但我没有知道它。

第一种方法在可读性方面更胜一筹,但是哪种方法对性能更好呢?

284172 次浏览

必须为两个备选方案执行的代码非常相似,以至于您无法可靠地预测结果。底层对象结构可能有所不同,但这对热点优化器来说不是挑战。因此,这取决于其他环境条件,如果有任何差异,这些条件将导致更快的执行。

组合两个过滤器实例会创建更多的对象,从而产生更多的委托代码,但如果你使用方法引用而不是lambda表达式,这可能会发生变化,例如用filter(ItemType::isCool)替换filter(x -> x.isCool())。这样就消除了为lambda表达式创建的合成委托方法。因此,使用两个方法引用组合两个过滤器可能会创建与使用带有&&的lambda表达式的单个filter调用相同或更少的委托代码。

但是,如前所述,HotSpot优化器将消除这种开销,并且可以忽略不计。

理论上,两个过滤器比一个过滤器更容易并行化,但这只适用于计算量相当大的任务¹。

所以没有简单的答案。

底线是,不要考虑气味检测阈值以下的性能差异。使用可读性更强的内容。


¹…并且需要一个实现对后续阶段进行并行处理,这是目前标准Stream实现没有走的路

这个测试表明,您的第二个选项可以表现得更好。首先是发现,然后是代码:

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=4142, min=29, average=41.420000, max=82}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=13315, min=117, average=133.150000, max=153}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10320, min=82, average=103.200000, max=127}

下面是代码:

enum Gender {
FEMALE,
MALE
}


static class User {
Gender gender;
int age;


public User(Gender gender, int age){
this.gender = gender;
this.age = age;
}


public Gender getGender() {
return gender;
}


public void setGender(Gender gender) {
this.gender = gender;
}


public int getAge() {
return age;
}


public void setAge(int age) {
this.age = age;
}
}


static long test1(List<User> users){
long time1 = System.currentTimeMillis();
users.stream()
.filter((u) -> u.getGender() == Gender.FEMALE && u.getAge() % 2 == 0)
.allMatch(u -> true);                   // least overhead terminal function I can think of
long time2 = System.currentTimeMillis();
return time2 - time1;
}


static long test2(List<User> users){
long time1 = System.currentTimeMillis();
users.stream()
.filter(u -> u.getGender() == Gender.FEMALE)
.filter(u -> u.getAge() % 2 == 0)
.allMatch(u -> true);                   // least overhead terminal function I can think of
long time2 = System.currentTimeMillis();
return time2 - time1;
}


static long test3(List<User> users){
long time1 = System.currentTimeMillis();
users.stream()
.filter(((Predicate<User>) u -> u.getGender() == Gender.FEMALE).and(u -> u.getAge() % 2 == 0))
.allMatch(u -> true);                   // least overhead terminal function I can think of
long time2 = System.currentTimeMillis();
return time2 - time1;
}


public static void main(String... args) {
int size = 10000000;
List<User> users =
IntStream.range(0,size)
.mapToObj(i -> i % 2 == 0 ? new User(Gender.MALE, i % 100) : new User(Gender.FEMALE, i % 100))
.collect(Collectors.toCollection(()->new ArrayList<>(size)));
repeat("one filter with predicate of form u -> exp1 && exp2", users, Temp::test1, 100);
repeat("two filters with predicates of form u -> exp1", users, Temp::test2, 100);
repeat("one filter with predicate of form predOne.and(pred2)", users, Temp::test3, 100);
}


private static void repeat(String name, List<User> users, ToLongFunction<List<User>> test, int iterations) {
System.out.println(name + ", list size " + users.size() + ", averaged over " + iterations + " runs: " + IntStream.range(0, iterations)
.mapToLong(i -> test.applyAsLong(users))
.summaryStatistics());
}
这是@Hank D共享的样本测试的6种不同组合的结果 显然,形式u -> exp1 && exp2的谓词在所有情况下都是高性能的
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=3372, min=31, average=33.720000, max=47}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9150, min=85, average=91.500000, max=118}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9046, min=81, average=90.460000, max=150}


one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8336, min=77, average=83.360000, max=189}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9094, min=84, average=90.940000, max=176}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10501, min=99, average=105.010000, max=136}


two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=11117, min=98, average=111.170000, max=238}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8346, min=77, average=83.460000, max=113}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9089, min=81, average=90.890000, max=137}


two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10434, min=98, average=104.340000, max=132}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9113, min=81, average=91.130000, max=179}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8258, min=77, average=82.580000, max=100}


one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9131, min=81, average=91.310000, max=139}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10265, min=97, average=102.650000, max=131}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8442, min=77, average=84.420000, max=156}


one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8553, min=81, average=85.530000, max=125}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8219, min=77, average=82.190000, max=142}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10305, min=97, average=103.050000, max=132}
从性能角度来看,复杂的过滤条件更好,但最好的性能将显示老式的for循环,标准if clause是最好的选择。对于一个小数组,10个元素的差异可能~ 2倍,对于一个大数组,差异没有那么大 你可以看看我的GitHub项目,在那里我做了多个数组迭代选项的性能测试

对于10个元素的小数组吞吐量ops/s: 10 element array 对于中等的10,000个元素吞吐量ops/s: enter image description here 对于大型阵列1,000,000个元素吞吐量ops/s: 1M elements < / p >

注意:测试在上面运行

  • 8 CPU
  • 1gb内存
  • 操作系统:16.04.1 LTS (Xenial Xerus)
  • Java版本:1.8.0_121
  • jvm: -XX:+UseG1GC -server -Xmx1024m -Xms1024m

< >强更新: Java 11在性能上有了一些进步,但动态保持不变

基准模式:吞吐量,ops/time Java 8vs11 < / p >