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로 바꾸면 출력 순서를 순차적으로 진행한 스트림처럼 보장할 수 있다.

  • 파이프라인이 수행하는 진짜 작업이 병렬화에 드는 추가 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다. 병렬화는 성능 최적화 수단이므로 성능을 테스트해서 병렬화를 사용할 가치가 있는지 판단해야한다.

  • 아래는 병렬화를 통해 효과를 보는 소수 계산 예제이다.

static long pi(long n) {
    return LongStream.rangeClosed(2, n)
            .parallel()
            .mapToObj(BigInteger::valueOf)
            .filter(i -> i.isProbablePrime(50))
            .count();
}
  • 무작위 수들로 이뤄진 스트림을 병렬화하기 위해 SplittedRandom 인스턴스를 활용하면 성능이 선형으로 증가하기 때문에 ThreadLocalRaodom, Random보다 낫다. Random을 사용하는 경우에는 모든 연산을 동기화하기 때문에 최악의 성능을 보일 것이다.

Last updated