스트림 병렬화는 주의해서 적용하라

김종준·2023년 7월 20일
0

이펙티브자바

목록 보기
41/63

스트림 병렬화는 주의해서 적용하라

환경이 아무리 좋더라도 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.

대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다.

이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기에 좋다는 특징이 있다.

나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Strem이나 Iterable의 spliterator 메서드로도 얻어올 수 있다.

이 자료구조들의 또 다른 중요한 공통점은 원소들을 순차적으로 실행할 때의 참조 지역성이 뛰어나다는 것이다.

이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다는 뜻이다.

하지만 참조들이 가리키는 실제 객체가 메모리에서 서로 떨어져 있을 수 있는데, 그러면 참조 지역성이 나빠진다.

참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 보내게 된다.

따라서 참조 지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할 때 아주 중요한 요소로 작용한다.

참조 지역성이 가장 뛰어난 자료구조는 기본 타입의 배열이다.

기본 타입 배열에서는 데이터 자체가 메모리에 연속해서 저장되기 때문이다

스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 준다.

종단 연산에서 수행하는 작업량이 파이프라인 전체 작업에서 상당 비중을 차지하면서 순차적인 연산이라면 파이프라인 병렬 수행의 효과는 제한될 수밖에 없다.

종단 연산 중 병렬화에 가장 적합한 것은 축소다.

축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업으로, Stream의 reduce 메서드 중 하나, 혹은 min, max, count, sum 같이 완성된 형태로 제공되는 메서드 중 하나를 선택해 수행한다.

anyMatch, allMatch, noneMatch 처럼 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다.

반면, 가변 축소를 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않다.

컬렉션들을 합치는 부담이 크기 때문이다.

직접 구현한 Stream, Iterable, Collection이 병렬화의 이점을 제대로 누리게 하고 싶다면 spliterator 메서드를 반드시 재정의하고 결과 스트림의 병렬화 성능을 강도 높게 테스트해야 한다.

스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못 한 동작이 발생할 수 있다.

결과가 잘못되거나 오작동하는 것은 안전 실패라 한다.

안전 실패는 병렬화한 파이프라인이 사용하는 mappers, fiters 혹은 프로그래머가 제공한 다른 함수 객체가 명세대로 동작하지 않을 때 벌어질 수 있다.

Stream 명세는 이때 사용되는 함수 객체에 관한 엄중한 규약을 정의해 놓았다.

예컨대 Stream의 reduce 연산에 건네지는 accumulator와 combiner 함수는 반드시 결합법칙을 만족하고, 간섭받지 않고, 상태를 갖지 않아야 한다.

이상의 요구사항을 지키지 못하는 상태라도 파이프라인을 순차적으로만 수행한다면야 올바를 결과를 얻을 수도 있다.

하지만 병렬로 수행하면 참혹한 실패로 이어지기 십상이다.

스트림 병렬화는 오직 성능 최적화 수단임을 기억해야 한다.

다른 최적화와 마찬가지로 변경 전후로 반드시 성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다.

0개의 댓글