요약
•
스트림을 병렬화 하려면 데이터 소스의 자료구조, 최종 연산의 종류, 안전 실패 등 여러 요소를 고려하여 진행하자.
스트림 병렬화의 한계
•
스트림 라이브러리에는 파이프라인을 병렬화하여 처리하는 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)
•
이와 같이 주어진 규약과 명세를 잘 지키지 않는다면, 순차적으로 동작할 때는 올바른 결과가 나올 수 있지만 병렬 수행 시 안전 실패가 발생할 수 있다.
병렬화 성능
•
스트림 병렬화는 다른 목적 없이 오직 성능 최적화만을 위한 동작이다. 그렇기에 다른 최적화처럼 적용 전과 후를 비교하여 병렬화할 가치가 있는지 확인해야 한다.
•
스트림 안의 원소 수와 원소당 수행되는 코드의 줄 수를 곱했을 때, 수십만 이상이 나와야 성능 향상을 제대로 누릴 수 있다.
•
일반적으로 수백줄짜리 코드를 관리하는 프로그램에서 병렬화를 적용할 일은 잘 없지만, 조건이 잘 갖춰진다면 프로세서 코어 수에 비례하는 성능향상을 기대할 수 있다.