Home [Effective Java] Item48. 스트림 병렬화는 주의해서 적용하라!
Post
Cancel

[Effective Java] Item48. 스트림 병렬화는 주의해서 적용하라!

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

Intro

  • 주류 언어 중, 동시성 프로그래밍 측면에서 자바는 항상 앞서갔다.
  • 처음 릴리즈된 1996년부터 스레드, 동기화, wait/notify를 지원했다.
  • 자바 5부터 동시성 컬렉션인 java.util.concurrent 라이브러리와 실행자(Executor) 프레임워크를 지원했다.
  • 자바 7부터 고성능 병렬 분해 프레임워크인 포크-조인 패키지를 추가했다.
  • 자바 8부터 parallel 메서드만 한 번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원했다.
  • 자바로 병렬 프로그래밍을 하는 것은 점점 쉬워지고 있다. 하지만 이를 올바른 방법으로 사용하는 것은 여전히 어렵다.
    • 동시성 프로그래밍 시 안전성(safety)과 응답 가능(liveness) 상태를 유지하기 위해 애써야 한다. 병렬 스트림 파이프라인 프로그램에서도 마찬가지다.

스트림 병렬화

문제점

  • 속도를 높이고 싶어 스트림 파이프라인의 parallel 메서드를 호출하겠다는 순진한 생각을 했다 치자.
    • 그럼 이 프로그램은 아무것도 하지 못하면서 CPU는 90%나 잡아먹는 상태가 무한히 계속된다. -> 응답 불가(liveness failure)
    • 왜 그럴까? 그 이유는 스트림 라이브러리가 파이프라인을 병렬화하는 방법을 찾지 못한 경우이기 때문이다.
      • 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
  • 파이프라인 병렬화는 limit를 다룰 때 CPU 코어가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다.
    • 그런데 마치 메르센 소수 계산 예제처럼, 원소 하나를 계산하는데 걸리는 시간이 이전까지의 원소 전부를 계산한 시간과 동일한 경우라면, 자동 병렬화 알고리즘은 제 기능을 못하게 된다.

교훈 : 스트림 파이프라인을 마구잡이로 병렬화하면 안 된다.

사용하면 좋은 경우

  • 병렬화의 효과가 가장 좋은 경우

    • 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스인 경우

    • 스트림의 소스가 int, long 범위의 배열인 경우

  • 이유는?

    • 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있다. 이는 다수의 스레드에 일을 분해하는 것이 용이하다는 것을 알 수 있다.
      • 분배는 Spliterator가 담당한다. 이 객체는 Stream이나 Iterable의 spliterator 메서드로 얻어올 수 있다.
    • 원소들을 순차적으로 실행할 때의 참조 지역성(locality of reference)이 뛰어나다.
      • 이웃한 원소들의 참조들이 메모리에 연속해서 저장되어 있다는 뜻이다.
      • 참조 지역성이 낮으면 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 멍하니 보내게 된다.
      • 참조 지역성이 가장 뛰어난 자료구조는 기본 타입의 배열이다. 기본 타입은 참조가 아닌 데이터 자체가 메모리에 연속 저장되기 때문이다.
  • 성능 상 이점?

    • 스트림 병렬화는 오직 성능 최적화를 위한 수단이다.
    • 병렬 스트림 파이프라인도 공통의 포크-조인 풀에서 수행되어 같은 스레드 풀을 사용해, 잘못된 파이프라인 하나가 시스템 다른 부분에까지 악영향을 줄 수 있지만, 조건이 잘 갖춰진다면 parallel 메서드 호출 하나로 거의 프로세서 고어 수준에 비례하는 성능 향상을 만끽할 수 있다.

종단 연산

  • 스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 준다. 종단 연산에서 수행하는 작업량이 전체 비중의 상당 부분을 차지하기 때문이다.
  • 종단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)다.
    • 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업을 말한다. Stream의 reduce 메서드 중 하나, 혹은 min, max, count, sum 등을 예로 들 수 있다.
  • anyMatch, allMatch, noneMatch처럼 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다.
  • 반면 가변 축소(mutable reduction)를 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않다.

고려 사항

  • 직접 구현한 Stream, Iterable, Collection이 병렬화의 이점을 제대로 누리게 하고 싶다면 spliterator 메서드를 반드시 재정의하자.
  • 성능 테스트를 강도 높게 진행해 성능을 확인하자.

안전 실패(safety failure)

  • 스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다.
    • 결과가 잘못되거나 오동작하는 것을 안전 실패라 한다.
  • 안전 실패는 병렬화한 파이프라인이 사용하는 mappers, filters, 혹은 프로그래머가 제공한 다른 함수 객체가 명세대로 동작하지 않을 때 벌어질 수 있다.
    • Stream 명세는 이때 사용되는 함수 객체에 관한 엄중한 규약을 정의해놨다. 예컨대 Stream의 reduce 연산에 건네지는 accumulator(누적기), combiner(결합기) 함수는 반드시 결합법칙을 만족하고, 간접받지 않고, 상태를 갖지 않아야 한다.

핵심 정리

  • 계산도 올바로 수행하고 성능도 빨라질 거라는 확신 없이는 스트림 파이프라인 병렬화는 시도조차 하지 말라.
  • 스트림을 잘못 병렬화하면 프로그램을 오동작하게 하거나 성능을 급격히 떨어뜨린다.
  • 병렬화하는 편이 낫다고 믿더라도, 수정 후의 코드가 여전히 정확한지 확인하자. 또 유사 조건에서 충분히 수행해보며 성능 지표를 확인하자.
  • 계산도 정확하고, 성능도 좋아졌음이 확실해졌을 때, 오직 그럴 때만 병렬화 버전을 운용하라.
This post is licensed under younghwani by the author.

[Effective Java] Item47. 반환 타입으로는 스트림보다 컬렉션이 낫다!

[Effective Java] Item49. 매개변수가 유효한지 검사하라!

Comments powered by Disqus.