lambda表现的差异?
这不是我的问题的重复。 我检查了它,它更多的是关于内部匿名类。
我对Lambda表达式很好奇并测试了以下内容:
- 给定一万个条目的数组,删除某些索引的速度会更快:Lamba表达式还是带有if测试的For-Loop?
第一个结果并不令人惊讶,因为我不知道我会想出什么:
final int NUMBER_OF_LIST_INDEXES = 10_000; List myList = new ArrayList(); String[] myWords = "Testing Lamba expressions with this String array".split(" "); for (int i = 0 ; i x.contains("s")); // 16 milliseconds for the traditional Loop for (int i = NUMBER_OF_LIST_INDEXES - 1 ; i >= 0 ; i--){ if (myList.get(i).contains("s")) myList.remove(i); } System.out.println(System.currentTimeMillis() - time + " milliseconds");
但是,我决定将常量NUMBER_OF_LIST_INDEXES
更改为一百万,这是结果:
final int NUMBER_OF_LIST_INDEXES = 1_000_000; List myList = new ArrayList(); String[] myWords = "Testing Lamba expressions with this String array".split(" "); for (int i = 0 ; i x.contains("s")); // 32854 milliseconds for the traditional Loop for (int i = NUMBER_OF_LIST_INDEXES - 1 ; i >= 0 ; i--){ if (myList.get(i).contains("s")) myList.remove(i); } System.out.println(System.currentTimeMillis() - time + " milliseconds");
为了使事情更简单,以下是结果:
| | 10.000 | 1.000.000 | | LAMBDA | 250ms | 390ms | 156% evolution |FORLOOP | 16ms | 32854ms | 205000+% evolution
我有以下问题:
-
这背后的魔力是什么? 当使用的索引是* 100时,我们如何为数组而不是lambda带来如此大的差异。
-
在性能方面,我们如何知道何时使用Lambdas以及何时坚持使用传统方式处理数据?
-
这是
List
方法的特定行为吗? 其他Lambda表达式是否也会产生像这样的随机性能?
我写了一个JMH基准来测量它。 它有4种方法:
- 在
ArrayList
上removeIf
。 -
LinkedList
上的removeIf
。 - 在
ArrayList
上使用iterator.remove()
迭代iterator.remove()
。 - 迭代器与
LinkedList
上的iterator.remove()
。
基准测试的目的是显示removeIf
和迭代器应提供相同的性能,但不是ArrayList
的情况。
默认情况下, removeIf
在内部使用迭代器来删除元素,因此我们应该期望使用removeIf
和iterator
具有相同的性能。
现在考虑一个ArrayList
,它在内部使用一个数组来保存元素。 每当我们调用remove
,索引后面的其余元素必须移动一个; 所以每次都要复制很多元素。 当迭代器用于遍历ArrayList
并且我们需要删除一个元素时,这种复制需要一次又一次地发生,这使得它非常慢。 对于LinkedList
,情况并非如此:删除元素时,唯一的更改是指向下一个元素的指针。
那么为什么removeIf
在ArrayList
上和LinkedList
上一样快? 因为它实际上被覆盖并且它不使用迭代器:代码实际上标记了要在第一遍中删除的元素,然后在第二遍中删除它们(移动其余元素)。 在这种情况下可以进行优化:每次需要移除剩余元素时,我们只会在知道需要删除的所有元素时执行一次。
结论:
- 当需要删除与谓词匹配的每个元素时,应使用
removeIf
。 -
remove
应该用于删除单个已知元素。
基准结果:
Benchmark Mode Cnt Score Error Units RemoveTest.removeIfArrayList avgt 30 4,478 ± 0,194 ms/op RemoveTest.removeIfLinkedList avgt 30 3,634 ± 0,184 ms/op RemoveTest.removeIteratorArrayList avgt 30 27197,046 ± 536,584 ms/op RemoveTest.removeIteratorLinkedList avgt 30 3,601 ± 0,195 ms/op
基准测试:
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(3) @State(Scope.Benchmark) public class RemoveTest { private static final int NUMBER_OF_LIST_INDEXES = 1_000_000; private static final String[] words = "Testing Lamba expressions with this String array".split(" "); private ArrayList arrayList; private LinkedList linkedList; @Setup(Level.Iteration) public void setUp() { arrayList = new ArrayList<>(); linkedList = new LinkedList<>(); for (int i = 0 ; i < NUMBER_OF_LIST_INDEXES ; i++){ arrayList.add(words[i%6]); linkedList.add(words[i%6]); } } @Benchmark public void removeIfArrayList() { arrayList.removeIf(x -> x.contains("s")); } @Benchmark public void removeIfLinkedList() { linkedList.removeIf(x -> x.contains("s")); } @Benchmark public void removeIteratorArrayList() { for (ListIterator it = arrayList.listIterator(arrayList.size()); it.hasPrevious();){ if (it.previous().contains("s")) it.remove(); } } @Benchmark public void removeIteratorLinkedList() { for (ListIterator it = linkedList.listIterator(linkedList.size()); it.hasPrevious();){ if (it.previous().contains("s")) it.remove(); } } public static void main(String[] args) throws Exception { Main.main(args); } }
因为remove(index)
非常昂贵:)它需要复制和移动其余的元素,这在你的情况下多次完成。
虽然removeIf(filter)
不需要这样做。 它可以扫描一次并标记要删除的所有元素; 然后最后阶段将幸存者复制到列表头部一次。
我认为你所看到的性能差异可能更多是因为removeIf
在内部使用迭代器而在for循环中使用get和remove。 这个PAQ中的答案有一些关于迭代器的好处的好信息。
bayou.io的答案是现货,你可以在这里看到removeIf的代码,它执行两次传递以避免一遍又一遍地移动剩余的元素。