개발자허허의 오늘은 뭐 먹지?

[JAVA] 반복문 vs stream vs parallelStream 중 뭐가 제일 빠를까? 본문

Dev/자세히 알아보기

[JAVA] 반복문 vs stream vs parallelStream 중 뭐가 제일 빠를까?

duck67 2021. 5. 3. 21:26
반응형

평소에 코딩할 때 간편하다는 이유로 stream을 많이 쓰는 편인데 list와 stream하고 비교하면 어떤 것이 더 속도가 빠를지 궁금해졌다. 그래서 반복문을 사용하는 것과 stream을 이용하는 것의 비교를 해보기로 했다.

    public static List<Integer> useFor(List<Integer> integerList) {
        List<Integer> res = new ArrayList<>();
        for(int i : integerList) {
            if(i % 2 == 0 && i % 3 == 0) {
                res.add(i);
            }
        }
        return res;
    }

    public static List<Integer> useStream1Filter(List<Integer> integerList) {
        List<Integer> res = integerList
                .stream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .collect(Collectors.toList());
        return res;
    }

    public static List<Integer> useParallelStreamFilter(List<Integer> integerList) {
        List<Integer> res = integerList
                .parallelStream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .collect(Collectors.toList());
        return res;
    }

    public static List<Integer> useStream2Filters(List<Integer> integerList) {
        List<Integer> res = integerList
                .stream()
                .filter(i-> i % 2 == 0)
                .filter(i -> i % 3 == 0)
                .collect(Collectors.toList());
        return res;
    }

리스트를 아래와 같은 갯수로 나눠서 만들고 테스트한 결과이다.

케이스 100,000건 10,000,000건 70,000,000건
useFor (반복문) 12 ms 137 ms 2338 ms
useStream1Filter (stream) 6 ms 117 ms 506 ms
useParallelStreamFilter (stream 병렬) 12 ms 117 ms 1730 ms ?!?!?!
useStream2Filters (stream, filter분리) 6 ms 188 ms 2352 ms

반복문과 스트림 중에는 스트림이 더 빠른 것 같다. 데이터가 많을 수록 더 좋은 결과를 보인다는 점은 어느정도 예상된 결과이기도 했다.

그리고 가독성을 위한다고 filter를 나누면 당연하게도... 성능이 떨어짐을 확인했다. 가급적 filter는 한방에 처리하기...

 

그런데 왜 paralleStream은 stream보다 처리 속도가 느릴까?

당연히 병렬로 처리하는데 더 빨라야 하는 것 아닌가?

 

혹시 리스트로 만들어주는 부분(collect 메서드 부분) 에서 문제가 있지 않을까해서 아래와 같이 collect대신 count만 세는 것으로 코드를 바꾸고 다시 테스트해보았다.

    public static long useFor(List<Integer> integerList) {
        long count = 0;
        for(int i : integerList) {
            if(i % 2 == 0 && i % 3 == 0) {
                count++;
            }
        }
        return count;
    }

    public static long useStream1Filter(List<Integer> integerList) {
        return integerList
                .stream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .count();
    }

    public static long useParallelStreamFilter(List<Integer> integerList) {
        return integerList
                .parallelStream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .count();
    }

결과는 아래와 같았다.

케이스 100,000건 10,000,000건 70,000,000건
useFor 16 ms 51 ms 669 ms
useStream1Filter 23 ms 51 ms 321 ms
useParallelStreamFilter 53 ms 55 ms 228 ms

1) 데이터의 갯수가 많으면 ParallelStream의 결과가 더 좋았고, 데이터가 적으면 오히려 ParallelStream의 성능이 좋지 않았다.

   병렬로 처리하다보면 아무래도 병렬처리를 위한 thread간 context switch처리나, 작업 결과를 merge하는 등의 overhead가 있을 수 있기 때문에 작은 규모의 데이터에서는 시간이 성능이 더 좋지 않을 수 있을수 있겠다.

2) 이 결과와 이전 테스트와 비교했을 때, stream 병렬처리를 collect 종료연산으로 처리할 경우 비용이 아주 커진다는 결론을 지을 수 있겠다.

 

그래서 병렬 처리 효과가 좋다고 알려진 reduce를 종료 연산으로해서 다시 한번 확인해보았다.

    public static long useFor(List<Integer> integerList) {
        long sum = 0;
        for(int i : integerList) {
            if(i % 2 == 0 && i % 3 == 0) {
                sum += i;
            }
        }
        return sum;
    }

    public static long useStream1Filter(List<Integer> integerList) {
        return integerList
                .stream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .reduce(0, Integer::sum);
    }

    public static long useParallelStreamFilter(List<Integer> integerList) {
        return integerList
                .parallelStream()
                .filter(i-> i % 2 == 0 && i % 3 == 0)
                .reduce(0, Integer::sum);
    }

결과는 아래와 같았다.

케이스 100,000건 10,000,000건 70,000,000건
useFor 12 ms 55 ms 334 ms
useStream1Filter 8 ms 123 ms 485 ms
useParallelStreamFilter 28 ms 74 ms 200 ms

바로 전 테스트결과와 유사했다.

 

왜 그런지 관련 내용은 Effective Java책을 뒤져보기로 했다.

이 책의 저자는 바로 바로...

위키백과 - 조슈아 블로치

Collection.java을 열어보면 최상단에 이름이 적혀있는 엄청난 형님이다. 이 분 말씀이 답이라고 보면 된다.

Collection.java

ArrayList, HashMap, HashSet, ConcurrentHashMap, 배열, int 범위연산, long 범위연산이 병렬처리에 적합하다. 왜냐하면 내부적으로 spliterator로 분할해서 처리하는데, 위 데이터 구조가 하위 범위로 분할할 때 비용이 적게 든다.

As a rule, performance gains from parallelism are best on streams over ArrayList, HashMap, HashSet, and ConcurrentHashMap instances; arrays; int ranges; and long ranges. What these data structures have in common is that they can all be accurately and cheaply split into subranges of any desired sizes, which makes it easy to divide work among parallel threads. The abstraction used by the streams library to perform this task is the spliterator, which is returned by the spliterator method on Stream and Iterable.

 

redcution, min, max, count, sum anyMatch, allMatch, noneMatch는 병렬처리에 적합한 terminal operation이다.

The best terminal operations for parallelism are reductions, where all of the elements emerging from the pipeline are combined using one of Stream’s reduce methods, or prepackaged reductions such as min, max, count, and sum. The short- circuiting operations anyMatch, allMatch, and noneMatch are also amenable to parallelism.

 

Stream의 collection 메소드는 mutable reduction인데, combine하는데 오버헤드가 크기 때문에 병렬처리에 좋지 않다.

The operations performed by Stream’s collect method, which are known as mutable reductions, are not good candidates for parallelism because the overhead of combining collections is costly.


<결론>

1) stream은 반복문보다 빠르다.

2) parallelStream은 병렬처리라서 반복문이나 stream보다 더 빠를 꺼 같지만 특정 상황에서 잘 써야 빠르다.

3) 잘 모르겠으면 속도를 측정해보고 쓸지 말지 판단하라.

 

추가로 parallelStream을 써야 한다면 아래 내용은 꼭 고민하도록 하자.

- Stream Collection을 같이 쓸 때는 비용이 많이 든다.

- 적은 양의 데이터에서는 오버헤드가 있어서 실익이 없을 수도 있다.

- 박싱은 성능을 크게 저하시키므로 IntStream, LongStream, DoubleStream을 쓸 것을 고려하도록 해라.

- 순서에 의존하는 연산(limit, findFirst)는 비용이 많이 들 수 있다.

- filter는 효과적이지 않을 수도 있다. 그 이유는 stream의 길이를 예측할 수 없어서이다.

- 병렬처리에 생각보다 잘 안맞는 메소드가 있을 수 있으므로 꼭 시간을 측정해보자.

반응형
Comments