[java] 자바 8 : 스트림과 컬렉션의 성능

저는 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)

이 경우 스트림이 더 성능이 좋습니다. 필터링 기능이 런타임 동안 한두 번만 호출되는 앱에서 무엇이 관찰 될지 궁금합니다.



답변

  1. LinkedList반복자를 사용하여 목록 중간에서 많이 제거하는 것 외에는 사용 을 중지하십시오 .

  2. 직접 벤치마킹 코드 작성을 중지하고 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는 모든 람다 항목을 인라인 할 수 있지만 바닐라 버전만큼 완벽하게 간결한 코드를 생성하지는 않습니다.

일반적으로 Java 8 스트림은 마법이 아닙니다. 그들은 이미 잘 구현 된 것들을 가속화 할 수 없었습니다 (아마도 일반 반복 또는 Java 5의 for-each 문으로 대체 Iterable.forEach()Collection.removeIf()호출). 스트림은 코딩 편의성과 안전성에 관한 것입니다. 편의성-스피드 트레이드 오프가 진행되고 있습니다.


답변

1) 벤치 마크를 사용하여 1 초 미만의 시간이 표시됩니다. 이는 결과에 부작용의 영향이 클 수 있음을 의미합니다. 그래서 당신의 작업을 10 배 늘 렸습니다

    int max = 10_000_000;

벤치 마크를 실행했습니다. 내 결과 :

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 = 1_000_000) 결과는

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)

그것은 큰 일입니다! ( int max = 10000000) 결론 : 컬렉션에 항목을 수집하는 시간의 대부분을했다. 가장 느린 부분이 목록에 추가됩니다. BTW는 단순 ArrayList에 사용됩니다 Collectors.toList().


답변

    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);
}

코드를 약간 변경하고 8 개의 코어가있는 맥북 프로에서 실행하면 합리적인 결과를 얻었습니다.

수집 : 경과 시간 : 1522036826ns (1.522037 초)

스트림 : 경과 시간 : 4315833719ns (4.315834 초)

병렬 스트림 : 경과 시간 : 261152901ns (0.261153 초)


답변

당신이하려는 일을 위해, 나는 일반적인 자바 API를 사용하지 않을 것입니다. 많은 boxing / unboxing이 진행되고 있으므로 성능 오버 헤드가 엄청납니다.

개인적으로 저는 많은 API가 많은 객체 쓰레기를 생성하기 때문에 쓰레기라고 생각합니다.

double / int의 기본 배열을 사용하고 단일 스레드를 수행하고 성능이 무엇인지 확인하십시오.

추신 : 벤치 마크를 처리하기 위해 JMH를 살펴볼 수 있습니다. JVM 예열과 같은 일반적인 함정을 처리합니다.


답변