Search

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

생성일
2023/08/01 11:27
챕터
7장 - 람다와 스트림

요약

스트림을 병렬화 하려면 데이터 소스의 자료구조, 최종 연산의 종류, 안전 실패 등 여러 요소를 고려하여 진행하자.

스트림 병렬화의 한계

스트림 라이브러리에는 파이프라인을 병렬화하여 처리하는 API를 제공하는데, 특정 조건에서는 파이프라인을 병렬화 할 방법을 찾지 못해 성능이 지나치게 느려진다.
데이터 소스가 Stream.iterate거나 중간 연산으로 limit을 사용하면, 파이프라인 병렬화를 사용하다가 성능이 지나치게 하락할 수 있다.
iterator는 기본적으로 순차적이기 때문에 병렬로 실행될 수 있는 청크로 분해하기 어렵고, 또한 iterator는 박싱된 객체를 생성하기 때문에 이를 언박싱하는 오버헤드가 추가된다.
limit 같은 경우에는 스트림이 병렬화 처리 중에 제한된 수 이후의 값들은 버려도 문제 없다고 판단을 하기 때문에, 이로 인해 불필요한 연산이 추가되거나 하여 오히려 성능이 안좋아질 수 있다.

스트림 병렬화 하기 좋은 소스

스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위 일 때 병렬화 효과가 가장 좋다.
위의 컬렉션들 및 자료구조들과 LinkedList나 Stream.iterator와 같은 자료구조의 차이는 분해성이 좋다는 것이다. 분해성이 좋아야 작업을 청크로 분해하여 병렬화 작업을 수행하기에 적합하다.
병렬화 하기 좋은 자료구조들의 또 다른 공통된 특징은 참조 지역성(locality of reference)가 좋다는 것이다. 이웃한 참조들이 메모리에 연속되어 저장되어 있기 때문에, 스레드가 주 메모리에서 캐시 메모리로 데이터 전송하는 시간이 적다.

스트림 파이프라인 최종 연산

스트림 파이프라인의 최종 연산 역시 스트림 병렬화의 효율에 영향을 준다.
reduce나 min, max, count, sum과 같은 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업인 축소(reduction)가 병렬화 효율이 좋다. 또한 anyMatch, allMatch, nonMatch와 같이 조건이 맞으면 바로 반환되는 메서드도 효율이 좋다.
반면, Stream의 collect와 같이 컬렉션들을 합치는 가변 축소(mutable reduction) 메서드들은 병렬화 효율이 좋지 않다.

안전 실패(safety failure)

스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라, 결과 자체가 잘못되거나 예상치 못한 동작이 발생할 수 있다. 이렇게 잘못된 결과를 반환하거나 오동작하는 것을 안전 실패(safety failure)라고 한다.
보통 안전 실패는 병렬화된 파이프라인이 사용하는 연산이 명세대로 동작하지 않을 때 발생할 수 있다.
reduce 연산에 건네지는 accumulator와 combiner는 반드시 지켜져야하는 몇 가지 규약이 있다.
결합 법칙을 만족해야한다. (a op b) op c == a op (b op c)
파이프라인이 수행되는 동안 데이터 소스가 변경되지 않아야 한다.
상태를 가지지 않아야 한다.(stateless)
이와 같이 주어진 규약과 명세를 잘 지키지 않는다면, 순차적으로 동작할 때는 올바른 결과가 나올 수 있지만 병렬 수행 시 안전 실패가 발생할 수 있다.

병렬화 성능

스트림 병렬화는 다른 목적 없이 오직 성능 최적화만을 위한 동작이다. 그렇기에 다른 최적화처럼 적용 전과 후를 비교하여 병렬화할 가치가 있는지 확인해야 한다.
스트림 안의 원소 수와 원소당 수행되는 코드의 줄 수를 곱했을 때, 수십만 이상이 나와야 성능 향상을 제대로 누릴 수 있다.
일반적으로 수백줄짜리 코드를 관리하는 프로그램에서 병렬화를 적용할 일은 잘 없지만, 조건이 잘 갖춰진다면 프로세서 코어 수에 비례하는 성능향상을 기대할 수 있다.