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


140

저는 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
당신은 그것을 시도 IntStream대신에?
Mark Rotteveel

2
제대로 측정 해 주실 수 있습니까? 당신이하고있는 모든 것이 한 번의 실행이라면, 벤치 마크는 물론 벗어날 것입니다.
skiwi

2
@MisterSmith 1K 테스트로 JVM을 예열하는 방법에 대한 투명성을 확보 할 수 있습니까?
Skiwi

1
그리고 올바른 마이크로 벤치 마크 작성에 관심이있는 사람들을 위해 여기에 질문이 있습니다 : stackoverflow.com/questions/504103/…
Mister Smith

2
@assylias toList쓰레드가 안전하지 않은 목록을 수집하더라도 스레드를 사용하기 전에 병렬로 실행해야합니다. 다른 스레드는 병합되기 전에 스레드가 포함 된 중간 목록으로 수집하기 때문입니다.
스튜어트 마크

답변:


192
  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()호출). 스트림은 코딩 편의성과 안전성에 관한 것입니다. 편의성-스피드 트레이드 오프가 진행되고 있습니다.


2
시간을내어 벤치마킹 해 주셔서 감사합니다. ArrayList에 대해 LinkedList를 변경하면 아무것도 변경되지 않을 것이라고 생각합니다. 두 테스트 모두 추가해야하므로 시간에는 영향을 미치지 않아야합니다. 어쨌든 결과를 설명해 주시겠습니까? 여기에서 무엇을 측정하고 있는지 말하기는 어렵습니다 (단위는 ns / op라고 말하지만 op로 간주되는 것은 무엇입니까?).
Mister Smith

52
성능에 대한 귀하의 결론은 유효하지만 과장되었습니다. 스트림 코드가 반복 코드보다 빠른 경우가 많이 있습니다. 주로 요소 별 액세스 비용이 일반 반복기보다 스트림에서 더 저렴하기 때문입니다. 그리고 많은 경우에, 스트림 버전은 손으로 쓴 버전과 동등한 것으로 인라인합니다. 물론, 악마는 세부 사항에 있습니다. 주어진 코드 비트는 다르게 동작 할 수 있습니다.
Brian Goetz

26
@BrianGoetz, 스트림이 더 빠를 때 사용 사례를 지정할 수 있습니까?
Alexandr

1
FMH의 마지막 버전에서 : use @Benchmark대신@GenerateMicroBenchmark
pdem

3
@BrianGoetz, 스트림이 더 빠를 때 사용 사례를 지정할 수 있습니까?
kiltek

17

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().


이 테스트를 마이크로 벤치 마크해야합니다. 즉, 먼저 여러 번 워밍업 한 다음 많은 tme를 실행하고 평균화해야합니다.
skiwi

@skiwi는 측정에 큰 편차가 있기 때문에 특히 옳습니다. 기본 조사 만 수행했으며 정확한 결과를 제시하지는 않습니다.
Sergey Fedorov

서버 모드의 JIT는 10k 실행 후 시작됩니다. 그런 다음 코드를 컴파일하고 교체하는 데 시간이 걸립니다.
pveentjer

이 문장에 관하여 : " 당신은 collect(Collectors.toList())당신이 명령에 즉, 당신이 많은 스레드에 의해 하나의 컬렉션을 해결해야 할 때 상황이있을 수 있습니다. "나는 거의 확신이 toList에를 수집 여러 가지 병렬 목록 인스턴스. 컬렉션의 마지막 단계로만 요소가 하나의 목록으로 전송 된 다음 반환됩니다. 따라서 동기화 오버 헤드가 없어야합니다. 그렇기 때문에 수집기에는 공급 업체, 누산기 및 결합기 기능이 모두 있습니다. (물론 다른 이유로 느려질 수 있습니다.)
Lii

@Lii 나는 collect구현 에 대해 같은 방식으로 생각합니다 . 그러나 결국 여러 목록이 단일 목록으로 병합되어야하며 주어진 예제에서 병합이 가장 무거운 작업 인 것처럼 보입니다.
Sergey Fedorov

4
    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 초)


나는 당신의 테스트가 공정하다고 생각합니다. 더 많은 CPU 코어가있는 기계가 필요합니다.
Mellon

3

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

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

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

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


LinkedList는 모든 Node 객체를 생성해야하기 때문에 ArrayList보다 더 나쁩니다. mod 연산자도 개 속도가 느립니다. 나는 10/15 사이클과 같은 것으로 + 명령 파이프 라인을 배수시킵니다. 2로 매우 빠른 나눗셈을하려면 숫자 1 비트를 오른쪽으로 이동하십시오. 이것들은 기본 트릭이지만 속도를 높이는 모드 고급 트릭이 있다고 확신하지만 더 문제가 될 수 있습니다.
pveentjer

나는 권투를 알고있다. 이것은 단지 비공식 벤치 마크입니다. 아이디어는 컬렉션 및 스트림 테스트에서 동일한 양의 복싱 / 언 박싱을 갖는 것입니다.
Mister Smith

먼저 실수를 측정하지 않는지 확인합니다. 실제 벤치 마크를 수행하기 전에 벤치 마크를 몇 번 실행하십시오. 그런 다음 적어도 JVM 워밍업이 중단되고 코드가 올바르게 정렬됩니다. 이것이 없으면 아마도 잘못된 결론을 내릴 것입니다.
pveentjer

좋아, 나는 당신의 조언에 따라 새로운 결과를 게시 할 것입니다. JMH를 살펴 보았지만 Maven이 필요하며 구성하는 데 시간이 걸립니다. 어쨌든 고마워
Mister Smith

나는 "당신이하려는 일에 대하여"벤치 마크 테스트를 피하는 것이 최선이라고 생각합니다. 즉, 일반적으로 이러한 종류의 운동은 설명하기에 충분할 정도로 단순 해지지 만 단순화 될 수있는 것처럼 보일 정도로 복잡합니다.
ryvantage
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.