item 48) 스트림 병렬화는 주의해서 적용하라
스트림 병렬화
자바 8부터 parallel() 메서드를 호출해 파이프라인을 병렬 실행할 수 있는 스트림을 지원한다.
동시성 프로그래밍과 병렬 스트림 파이프라인 프로그래밍에서는 안전성과 응답 가능 상태를 유지하기 위해 노력해야 한다.
병렬화 사용하면 안되는 상황
데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 병렬화로 성능 개선이 불가하다.
병렬화는 limit를 다룰 때 CPU 코어가 남는다면 원소를 여럿 더 처리한 후에 제한 갯수 이후의 결과를 버려도 무방하다고 가정한다. 만약 하나의 원소를 처리하는 데에 많은 비용이 든다면, 이렇게 여럿 더 처리하는 것이 성능에 치명적이다.
병렬화에 적합한 자료구조
ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스, 배열, int ~ long 범위일 때 병렬화의 효과가 가장 좋다.
데이터를 Spliterator가 원하는 크기로 정확하고 손쉽게 나눌 수 있어 다수 스레드에 분배하기 좋다.
원소들을 순차적 실행할 때 참조 지역성이 뛰어나다. 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있어야 주 메모리와 캐시 메모리 사이의 데이터 전송 빈도를 줄일 수 있으므로 다량의 데이터를 처리하는 벌크 연산을 병렬화할 때 중요하다.
병렬화와 종단 연산
병렬화에 가장 적합한 종단 연산은 축소(스트림 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업)이다. 축소 작업은 Stream의 reduce 메서드 혹은 min, max, count, sum 같이 완성된 형태로 제공되는 메서드 중 하나를 선택해서 수행한다.
anyMatch, allMatch, noneMatch처럼 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다.
가변 축소를 수행하는 collect 메서드는 컬렉션들을 합치는 부담이 커 병렬화에 적합하지 않다.
안전 실패
스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라, 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다.
병렬화한 파이프라인이 사용하는 mappers, filters 등의 함수 객체가 요구사항을 지키지 못한 상태에서 명세대로 동작하지 않을 때 벌어질 수 있다.
Stream 명세는 함수 객체에 관한 엄중한 규약을 정의해놨다. ex) reduce 연산에 건네지는 누적기와 결합기 함수는 반드시 결합 법칙을 만족하고, 간섭받지 않고, 상태를 갖지 않아야 한다.
병렬화 팁
forEach를 forEachOrdered로 바꾸면 출력 순서를 순차적으로 진행한 스트림처럼 보장할 수 있다.
파이프라인이 수행하는 진짜 작업이 병렬화에 드는 추가 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다. 병렬화는 성능 최적화 수단이므로 성능을 테스트해서 병렬화를 사용할 가치가 있는지 판단해야한다.
아래는 병렬화를 통해 효과를 보는 소수 계산 예제이다.
무작위 수들로 이뤄진 스트림을 병렬화하기 위해 SplittedRandom 인스턴스를 활용하면 성능이 선형으로 증가하기 때문에 ThreadLocalRaodom, Random보다 낫다. Random을 사용하는 경우에는 모든 연산을 동기화하기 때문에 최악의 성능을 보일 것이다.
Last updated