[아이템 48] 스트림 병렬화는 주의해서 사용하라

2023. 5. 31. 16:14Java/Effective Java

동시성 프로그래밍

  • 동시성 프로그래밍을 할때는 항상 안전성(safety)와 응답 가능(liveness) 상태를 유지해야 하는 것에 주의해야 한다.

 

 

동시성 프로그래밍 주의점

예제) 메르센 소수를 생성하는 프로그램

  • parallel() 사용 ⇒ 프로그램은 아무것도 출력하지 못하면서 CPU는 90%나 잡아먹는 상태 나타남
  • 스트림 라이브러리가 이 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다.
  • 아래의 코드의 경우, 새롭게 메르센 소수를 찾을 때마다 그 전 소수를 찾을 때 보다 두배 정도 더 오래 걸린다. 원소 하나를 계산하는 비용이 대략 그 이전까지의 원소 전부를 계산한 비용을 합친 것만큼 든다는 뜻
public static void main(String[] args) {
	primes()
        .parallel()
        .map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
        .filter(mersenne -> mersenne.isProbablePrime(50))
        .limit(20)
        .forEach(System.out::println);
}

static Stream<BigInteger> primes() {
	return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

 

 

스트림 병렬화

  • 스트림에서 병렬화 사용 시에 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
  • 스트림 파이프라인을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 예상치 못한 동작이 발생한다. (안전실패(safety failure))
  • 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다.
    • 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기에 좋다는 특징이 있다.
    • 나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream이나 Iterable의 spliterator 메서드로 얻어올 수 있다.
    • 이 자료구조들의 또 다른 중요한 공통점은 원소들을 순차적으로 실행할 때의 참조 지역성(locality of reference)이 뛰어나다는 것이다.

 

 

※ 참조의 지역성

  • 캐시 메모리는 주기억장치와 메모리 사이에 위치하며 속도 차이에 따른 병목현상을 줄여준다.
  • 캐시 메모리의 효율을 높이려면 캐시 메모리에 어떤 정보가 들어있냐가 중요. 어떤 프로그램을 실행할 때 필요한 정보를 캐시 메모리에서 찾는 캐시 적중율(hit-rate)을 높일 수 있기 때문이다.
  • 이러한 캐시 적중율을 높일 수 있는 원리가 있는데, 그것이 바로 참조의 지역성이다.
  • 참조의 지역성이란 컴퓨터 프로그램이 일정 기간 동안 특정한 메모리 위치 집합에 접근하는 경향이 있는 현상을 뜻한다. 쉽게 말해 참조의 지역성은 주소가 서로 가까운 명령어에 접근하는 경향을 나타낸다
  • 즉, 참조의 지역성이란 동일한 값 또는 해당 값에 관계된 스토리지 위치가 자주 액세스되는 특성으로, 지역성의 원리(principle of locality)라고도 불린다.
  • 종류
    • 시간적 지역성 : 시간적 지역성은 최근 참조된 명령어나 데이터가 곧 다시 필요할 수 있다는 것을 의미
    • 공간적 지역성 : 공간적 지역성은 현재 메모리 위치에 가까운 명령어나 데이터가 곧 필요할 수 있다는 것을 의미
    • 순차적 지역성 : 데이터가 순차적으로 액세스되는 경향으로, 프로그램 내의 명령어가 순차적으로 구성되어 있다는 것이 대표적인 경우

 

 

스트림 파이프라인의 종단 연산

  • 스트림 파이프라인의 종단 연산의 동작 방식 역시 병려 수행 효율에 영향을 미친다.
  • 만약 종단 연산에서 수행하는 작업량이 파이프라인 전체 작업에서 상당 비중을 차지하면서 순차적인 연산이라면, 파이프라인 병렬 수행의 효과는 제한될 수 밖에 없다.
  • 종단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)다.
    • 축소는 파이프라인에서 만들어진 원소를 하나로 합치는 작업
  • min, max, count, sum, anyMatch, allMatch, noneMatch 처럼 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다.
  • 반면, 가변 축소를 수행하는 Stream의 collect 메서드는 병렬화에 적합하지 않다.
    • 가변 축소 메서드란? 결과값을 가공하여 새로운 리스트로 추출하는 메서드이다.

 

 

정리

  • 올바른 수행과 성능 향상의 확신이 없이는 스트림 파이프라인 병렬화를 사용하지 말자.
  • 스트림을 잘못 병렬화하면 오동작이 발생하거나 성능을 급격히 떨어뜨린다.
  • 병렬화하는게 낫다고 생각해도 테스트를 통해 성능지표를 관찰하라.