我是Java 8的新手。我仍然不深入了解API,但我已经做了一个小的非正式基准测试来比较新Streams API与旧版的性能集合。
测试包括过滤Integer
列表,对于每个偶数,计算平方根并将其存储在List
的结果Double
中。
以下是代码:
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.
以下是双核机器的结果:
Collections: Elapsed time: 94338247 ns (0,094338 seconds)
Streams: Elapsed time: 201112924 ns (0,201113 seconds)
Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds)
对于这个特定的测试,流的速度大约是集合的两倍,并行性没有帮助(或者我用错误的方式使用它?)。
问题:
更新了结果。
按照@pveentjer的建议,我在JVM预热(1k次迭代)后运行了1k次测试:
Collections: Average time: 206884437,000000 ns (0,206884 seconds)
Streams: Average time: 98366725,000000 ns (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds)
在这种情况下,流更高效。我想知道在一个应用程序中会观察到什么,在运行时只调用过滤函数一次或两次。
答案 0 :(得分:163)
停止使用LinkedList
除了使用迭代器从列表中间删除大量内容之外的任何内容。
停止手动编写基准测试代码,使用JMH。
适当的基准:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
public static final int N = 10000;
static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}
@Benchmark
public List<Double> vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}
@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}
}
结果:
Benchmark Mode Samples Mean Mean error Units
StreamVsVanilla.stream avgt 10 17.588 0.230 ns/op
StreamVsVanilla.vanilla avgt 10 10.796 0.063 ns/op
正如我预期的那样,流实现速度相当慢。 JIT能够内联所有lambda内容,但不会像vanilla版本那样生成完美简洁的代码。
通常,Java 8流不是魔术。他们无法加速已经很好实现的东西(可能是普通的迭代或Java 5的 - 每个语句都替换为Iterable.forEach()
和Collection.removeIf()
调用)。流更多的是编码方便性和安全性。方便 - 速度权衡在这里工作。
答案 1 :(得分:12)
1)您使用基准测试时间不到1秒。这意味着副作用会对您的结果产生强烈影响。所以,我增加了你的任务10次
int max = 10000000;
并运行您的基准测试。我的结果:
Collections: Elapsed time: 8592999350 ns (8.592999 seconds)
Streams: Elapsed time: 2068208058 ns (2.068208 seconds)
Parallel streams: Elapsed time: 7186967071 ns (7.186967 seconds)
没有编辑(int max = 1000000
)结果
Collections: Elapsed time: 113373057 ns (0.113373 seconds)
Streams: Elapsed time: 135570440 ns (0.135570 seconds)
Parallel streams: Elapsed time: 104091980 ns (0.104092 seconds)
它与您的结果相似:流比收集慢。 结论:花费了很多时间进行流初始化/值传输。
2)增加任务流后变得更快(“没问题”),但并行流仍然太慢。怎么了?注意:您的命令中有collect(Collectors.toList())
。收集单个集合实质上会在并发执行时引入性能瓶颈和开销。可以通过替换
collecting to collection -> counting the element count
对于流,可以通过collect(Collectors.counting())
完成。我得到了结果:
Collections: Elapsed time: 41856183 ns (0.041856 seconds)
Streams: Elapsed time: 546590322 ns (0.546590 seconds)
Parallel streams: Elapsed time: 1540051478 ns (1.540051 seconds)
这&#39;是一项重大任务! (int max = 10000000
)结论:将收集的项目收集到大部分时间。最慢的部分是添加到列表中。顺便说一句,简单的ArrayList
用于Collectors.toList()
。
答案 2 :(得分:3)
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 10000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add( doSomeCalculate(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}
static double doSomeCalculate(int input) {
for(int i=0; i<100000; i++){
Math.sqrt(i+input);
}
return Math.sqrt(input);
}
我稍微更改了代码,在我的mac book pro上运行了8个核心,我得到了一个合理的结果:
收藏集:已用时间:1522036826 ns(1.522037秒)
Streams:已用时间:4315833719 ns(4.315834秒)
并行流:经过时间:261152901 ns(0.261153秒)
答案 3 :(得分:1)
对于你想要做的事情,我不会使用常规的java api。有大量的装箱/拆箱,因此存在巨大的性能开销。
就我个人而言,我认为很多API设计都是废话,因为它们会产生大量的对象垃圾。
尝试使用double / int的原始数组,并尝试单线程并查看性能是什么。
PS: 您可能希望了解JMH以处理基准测试。它解决了一些典型的陷阱,如加热JVM。
答案 4 :(得分:1)
Java 8 和 Java 11 的有趣结果。我使用了由 leventov 提供的代码,几乎没有修改:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(BenchmarkMain.N)
public class BenchmarkMain {
public static final int N = 10000;
static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}
@Benchmark
public List<Double> vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}
@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {
org.openjdk.jmh.Main.main(args);
}
}
Java 8:
# JMH version: 1.31
# VM version: JDK 1.8.0_262, OpenJDK 64-Bit Server VM, 25.262-b19
# VM invoker: /opt/jdk1.8.0_262/jre/bin/java
# VM options: <none>
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
...
Benchmark Mode Cnt Score Error Units
BenchmarkMain.stream avgt 25 10.680 ± 0.744 ns/op
BenchmarkMain.vanilla avgt 25 6.490 ± 0.159 ns/op
Java 11:
# JMH version: 1.31
# VM version: JDK 11.0.2, OpenJDK 64-Bit Server VM, 11.0.2+9
# VM invoker: /opt/jdk-11.0.2/bin/java
# VM options: <none>
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
...
Benchmark Mode Cnt Score Error Units
BenchmarkMain.stream avgt 25 5.521 ± 0.057 ns/op
BenchmarkMain.vanilla avgt 25 7.359 ± 0.118 ns/op