Item48. Stream 병렬화는 주의해서 적용하라
- jdk 8 부터는 parallel method만 한번 호출하면 pipeline을 병렬 실행할수 있는 stream을 지원하기 시작했다.
- stream 병렬화는 함부로 사용하면 오히려 성능 저하를 야기할 수 있다.
아래와 같아 처음 20개의 메르센 소수를 생성하는 프로그램이 있다고 가정하자
1 | public static void main(String[] args) { |
이를 병렬로 처리하고자 stream pipeline의 .parallel() method를 호출하면 오히려 프로그램 수행 시간이 극단적으로 느려지게 된다.
그 이유는 stream library 가 pipeline을 병렬화하는 방법을 찾지 못했기 때문인데, 데이터 소스가 stream.iterate거나,중간 연산으로 limit를 쓰면 pipeline병렬화로는 성능개선을 기대할 수 없다.
limit을 다룰때 CPU 코어개수가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정하는데, 위 메르센소수를 찾는 프로그램은 원소하나의 계산비용이 그 원소 이전에 계산한 모든 원소들의 비용을 합친 것 만큼 든다.
즉 메르센 소수를 찾는 프로그램의 경우 몇 개 더 처리한 후 제한된 개수 이후의 결과를 얻는 불필요한 비용 자체도 많이 든다.
Stream 병렬화 효과가 좋은 데이터
대체로 Stream의 Source가 ArrayList , HashMap , HashSet , ConcurrentHashMap 의 인스턴스거나 , 배열일때 병렬화의 효과가 가장 좋다.
위 병렬화에 적합한 자료구조들은 다음과 같은 특징을 가지고 있다.
원하는 크기로 정확하게 손쉽게 나눌 수 있다. 즉 다수의 thread에게 일을 분배하기 좋다는 특징이 있다.
참조 지역성(locality of reference)이 뛰어나다. 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다 (공간 지역성)
만약 참조 지역성이 낮으면 Thread는 데이터가 메모리로 전송될떄까지 대기하게 된다.
따라서 참조지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할때 아주 중요한 요소로 작동하는데, 참조 지역성이 가장 뛰어난 자료구조는 기본 타입 배열이다.
(참조 지역성이란 기억 장치(SSD,HDD)로부터 데이터가 참조될때, 시간적/공간적/순차적으로 분포가 집중되는 성질 )
병렬화에 적합한 종단 연산
Stream pipeline에서는 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 준다.
종단 연산에서 수행하는 작업량이 pipeline 전체 작업에서 상당 비중을 차지하면서 순차적인 연산이라면 병렬 수행 효과는 제한 될 수 밖에 없다.
축소(reduction).
축소는 pipeline에서 만들어진 모든 원소를 하나로 합치는 작업으로, Stream의 reduce method중 하나 , 혹은 min , max , count , sum과 같이 완성된 형태로 제공되는 method 중에 하나를 선택해 수행한다.anyMatch,allMatch,noneMatch 처럼 조건에 맞으면 바로 반환되는 method
반면 가변 축소를 수행하는 Stream의 collect method는 병렬화에 적합치 않은데, collection을 합치는 부담이 크기 떄문이다.
추가로 병렬화 성능을 높이고 싶다면 spliterator를 overriding 하고, 결과 stream의 병렬화 성능을 테스트하는 방안이 있다.
안전 실패 (safety failure)
stream을 잘못 병렬화하면 성능 뿐 아니라 결과 자체도 원하지 않는 결과가 나올 수 있는데, 결과가 잘못되거나 오작동 하는 것을 안전 실패라고 한다.
안전 실패는 병렬화한 pipeline이 사용하는 mapper , filter 혹은 programmer가 제공한 다른 함수 객체가 명세대로 동작하지 않을떄 벌어질 수 잇다. 따라서 Stream 명세에 적힌 함수 객체에 대한 규칙을 지켜서 제공해야 한다.
예를 들면 Stream의 reduce 연산에 건내지는 accumulator , combiner 함수는 반드시 아래와 같은 규칙을 만족해야한다.
- 결합 법칙 만족
- 간섭받지 않아야 함
- 상태를 갖지 말아야 한다.
stream 병렬화는 오직 성능 최적화 수단이다. 따라서 다른 최적화와 마찬가지로 변경 전후에 성능을 테스트해서 병렬화했을때, 성능이 올라갔는지 확인해보아야 한다.
좋은 병렬화 예시
다음은 n보다 작거나 같은 소수의 개수를 계산하는 함수이다.
1 | static long pi(long n){ |
이를 병렬화시키면 약 3배 정도의 성능향상을 기대할 수 있다.
1 | static long pi(long n){ |
추가로 무작위로 이루어진 stream을 병렬화할떄는 ThreadLocalRandom,Random 보다는 SplittableRandom 을 사용하면 병렬화시 성능적 장점을 가질 수 있다.