用于查找列表

时间:2015-09-02 07:23:29

标签: java performance java-8 java-stream

我编写了一个简单的程序,用于比较流的性能,以查找整数的最大形式列表。令人惊讶的是,我发现了'流的方式'通常方式的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);


}

}

3 个答案:

答案 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不断尝试优化性能,因此将使用通常的方式设置缓存。如果你在流方式之前以通常的方式运行,你应该得到相反的结果。我建议在不同的主电源上运行测试以获得最佳效果。