의의

parallelStream() 으로 병렬 스트림으로 만들면 알아서 연산을 병렬로 처리해 준다.
그렇다면 성능 개선을 위해 모든 스트림을 병렬 스트림으로 쓰면 되지 않을까? 라는 생각을 할 수 있다.
직접 벤치마크를 통해 병렬 스트림을 사용했을 때의 성능을 확인해 보자.

환경설정

벤치마크는 OpenJdk에서 개발한 JMH(Java MicroBench Harness)를 사용했다.

세팅은 프로젝트의 gradle/wrapper/gradle-wrapper.properties 파일에서 gradle 버전을 확인하고, jmh-gradle-plugin 를 참고하여 버전에 맞게 build.gradle의 plugins를 추가해 주면 된다.

벤치마크

다음 코드는 10_000_000L 까지 for문을 통해 덧셈을 하는 벤치마크 코드이다.
여러 어노테이션이 보이는데 대부분 어떻게 벤치마크 할 것인지 설정이기 때문에 각자 원하는 대로 사용하면 된다. 중요한 것은 @Benchmark 가 붙어있는 부분만 보면 된다.

단순 for문

@State(Scope.Benchmark)  
@BenchmarkMode(Mode.AverageTime)  
@OutputTimeUnit(TimeUnit.MICROSECONDS)  
@Fork(value = 1, jvmArgs = {"-Xms4G", "-Xmx4G"})  
public class StreamBenchMark {  

    public static final long N = 10_000_000L;  

    @Benchmark  
    public long iterativeSum() {  
        long result = 0;  
        for (long i = 1L; i <= N; i++) {  
            result += i;  
        }  
        return result;  
    }

    @TearDown(Level.Invocation)  
    public void tearDown() {  
        System.gc();  
    }  
}

단순한 for문과 순차 스트림과의 성능을 비교해 보자.

순차 스트림

@Benchmark  
public long sequentialSum() {  
    return LongStream.iterate(1L, i -> i + 1).limit(N)  
            .reduce(0L, Long::sum);  
}

역시 for문은 스트림을 생성하는 등의 비용도 없기 때문에 훨씬 빠르다는 것을 확인할 수 있다.

그렇다면 병렬 스트림은 어떨까?

병렬 스트림

@Benchmark  
public long parallelSum() {  
    return LongStream.iterate(1L, i -> i + 1).limit(N)  
            .parallel()  
            .reduce(0L, Long::sum);  
}

병렬 스트림을 사용했지만 순차 스트림보다 성능이 떨어졌다.
병렬 스트림을 사용하여 작업을 병렬로 처리하면 무조건 성능이 좋을 것이라 생각할 수 있지만 이는 잘못된 생각일 수 있다. 위 코드는 iterate로 무한 스트림을 생성하기 때문에 limit로 제한을 두더라도 여전히 스트림은 무한해서 병렬로 처리하도록 청크로 분할할 수 없다. 따라서 병렬로 처리하기 위한 오버헤드만 겪게 되고 오히려 성능적으로 손해 보는 현상이 일어난다.

따라서, 위 문제를 해결하기 위해서는 다음과 같이 rangeClosed() 로 쉽게 청크로 분할할 수 있는 범위를 제한할 수 있다.

범위를 제한한 병렬 스트림

@Benchmark  
public long rangedParallelSum() {  
    return LongStream.rangeClosed(1, N)  
            .reduce(0L, Long::sum);  
}

rangeClosed()를 통해 범위를 제한하니 순차 스트림보다 성능이 좋아지는 것을 확인할 수 있다. 하지만 눈에 띄는 성능 개선인지는 느껴지지 않는다. 어떤 연산을 하느냐에 따라서 병렬로 처리했을 때 성능 차이가 있기 때문이다. 어떤 연산을 하는지도 병렬로 처리할지에 대한 고려 요소 중 하나이다.

결론

이러한 문제 외에도 공유된 상태를 변경하거나, 데이터가 적거나 사용하는 자료구조에 따른 차이 등 병렬 스트림을 잘못 사용하면 문제가 발생하거나 성능이 오히려 떨어질 수 있다.
따라서 병렬 스트림을 남발하는 것이 아니라 구조를 잘 이해하고, 성능 개선이 확실하지 않으면 직접 벤치마크를 통해서 성능을 측정해 보자.

+ Recent posts