我编写了一个简单的程序,用于比较流的性能,以查找整数的最大形式列表。令人惊讶的是,我发现了'流的方式'通常方式的1/10'。难道我做错了什么?是否存在Stream方式无效的条件?任何人都可以对这种行为有一个很好的解释吗?
"流式方式"耗时80毫秒"通常的方式"花了15毫秒 请找到以下代码
public class Performance {
public static void main(String[] args) {
ArrayList<Integer> a = new ArrayList<Integer>();
Random randomGenerator = new Random();
for (int i=0;i<40000;i++){
a.add(randomGenerator.nextInt(40000));
}
long start_s = System.currentTimeMillis( );
Optional<Integer> m1 = a.stream().max(Integer::compare);
long diff_s = System.currentTimeMillis( ) - start_s;
System.out.println(diff_s);
int e = a.size();
Integer m = Integer.MIN_VALUE;
long start = System.currentTimeMillis( );
for(int i=0; i < e; i++)
if(a.get(i) > m) m = a.get(i);
long diff = System.currentTimeMillis( ) - start;
System.out.println(diff);
}
}
答案 0 :(得分:4)
是的,Streams对于这种简单的操作来说速度较慢。但你的数字完全不相关。如果您认为15毫秒是您的任务的满意时间,那么有一个好消息:热身流代码可以在0.1-0.2毫秒内解决这个问题,这比70-150倍快。
这是一个快速而肮脏的基准:
import java.util.concurrent.TimeUnit;
import java.util.*;
import java.util.stream.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.annotations.*;
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(3)
@State(Scope.Benchmark)
public class StreamTest {
// Stream API is very nice to get random data for tests!
List<Integer> a = new Random().ints(40000, 0, 40000).boxed()
.collect(Collectors.toList());
@Benchmark
public Integer streamList() {
return a.stream().max(Integer::compare).orElse(Integer.MIN_VALUE);
}
@Benchmark
public Integer simpleList() {
int e = a.size();
Integer m = Integer.MIN_VALUE;
for(int i=0; i < e; i++)
if(a.get(i) > m) m = a.get(i);
return m;
}
}
结果是:
Benchmark Mode Cnt Score Error Units
StreamTest.simpleList avgt 30 38.241 ± 0.434 us/op
StreamTest.streamList avgt 30 215.425 ± 32.871 us/op
这是 micro 秒。所以Stream版本实际上比你的测试快得多。然而,简单版本更快。因此,如果你在15毫秒内没问题,你可以使用你喜欢的这两个版本中的任何一个:两者都会表现得更快。
如果你想获得最佳性能,无论如何,你应该摆脱盒装Integer
对象并使用原始数组:
int[] b = new Random().ints(40000, 0, 40000).toArray();
@Benchmark
public int streamArray() {
return Arrays.stream(b).max().orElse(Integer.MIN_VALUE);
}
@Benchmark
public int simpleArray() {
int e = b.length;
int m = Integer.MIN_VALUE;
for(int i=0; i < e; i++)
if(b[i] > m) m = b[i];
return m;
}
现在两个版本都更快:
Benchmark Mode Cnt Score Error Units
StreamTest.simpleArray avgt 30 10.132 ± 0.193 us/op
StreamTest.streamArray avgt 30 167.435 ± 1.155 us/op
实际上流版本的结果可能会有很大差异,因为它涉及许多在不同时间进行JIT编译的中间方法,因此在一些迭代之后速度可能会在任何方向上发生变化。
顺便说一句,原始问题可以通过没有Stream API的良好旧Collections.max
方法解决,如下所示:
Integer max = Collections.max(a);
通常,您应该避免测试无法解决实际问题的人工代码。使用人工代码,您将获得人为结果,这些结果通常与实际条件下的API性能无关。
答案 1 :(得分:1)
我看到的直接差异是流方式使用Integer :: compare,这可能需要更多的自动装箱等,而不是循环中的运算符。也许你可以在循环中调用Integer :: compare来查看这是否是原因?
编辑:根据尼古拉斯罗宾逊的建议,我写了一个新版本的测试。它使用400K大小的列表(原始列表产生零差异结果),它在两种情况下都使用Integer.compare,并且在每次调用时只运行其中一个(我在两种方法之间交替):static List<Integer> a = new ArrayList<Integer>();
public static void main(String[] args)
{
Random randomGenerator = new Random();
for (int i = 0; i < 400000; i++) {
a.add(randomGenerator.nextInt(400000));
}
long start = System.currentTimeMillis();
//Integer max = checkLoop();
Integer max = checkStream();
long diff = System.currentTimeMillis() - start;
System.out.println("max " + max + " diff " + diff);
}
static Integer checkStream()
{
Optional<Integer> max = a.stream().max(Integer::compare);
return max.get();
}
static Integer checkLoop()
{
int e = a.size();
Integer max = Integer.MIN_VALUE;
for (int i = 0; i < e; i++) {
if (Integer.compare(a.get(i), max) > 0) max = a.get(i);
}
return max;
}
循环的结果:max 399999 diff 10
流的结果:max 399999 diff 40
(有时我得到50
)
答案 2 :(得分:0)
在Java 8中,他们一直在努力利用新lambda的并发进程。您会发现流更快,因为列表正以最有效的方式同时处理,而通常的方式是按顺序运行列表。
因为lambda是静态的,这使得线程更容易,但是当你访问硬盘驱动器的某些内容(逐行读取文件)时,您可能会发现该流不会有效,因为硬盘驱动器只能访问信息
[UPDATE] 你的流比正常方式花费的时间长得多的原因是因为你先跑了。 JRE不断尝试优化性能,因此将使用通常的方式设置缓存。如果你在流方式之前以通常的方式运行,你应该得到相反的结果。我建议在不同的主电源上运行测试以获得最佳效果。