티스토리 뷰

728x90

본 포스팅은 이펙티브 자바 3/E아이템 48. 스트림 병렬화는 주의해서 적용하라를 읽고 정리한 내용이다.

아이템 45. 스트림은 주의해서 사용하라
기본적으로 스트림 파이프라인은 순차적으로 수행된다.
파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출해주기만 하면 되나, 효과를 볼 수 있는 상황은 많지 않다.

1. 계산 결과가 정확하고 2. 성능도 좋아질 것이라는 확신이 없다면 스트림 병렬화를 적용하지 말자!!

계산량이 많거나 빅데이터를 처리하거나 성능 최적화가 필요한 상황에서 스트림 병렬화는 매력적이게 보일 수 있다.
모든 동시성 프로그래밍에서는 안전성(safety)응답 가능(liveness)상태를 유지해야 하는데,
스트림을 잘못 병렬화하면 오히려 프로그램을 오작동 혹은 응답 불가하게 만들거나 성능을 급격히 떨어뜨릴 수 있다.

소수 계산 프로그램 👍

n보다 작거나 같은 소수의 개수를 계산한다.

@Test
void countPrime() {
    System.out.println(pi(100_000_000));
}

private long pi(long n) {
    return LongStream.rangeClosed(2, n)
            .parallel() // 스트림 병렬화
            .mapToObj(BigInteger::valueOf)
            .filter(i -> i.isProbablePrime(50))
            .count();
}
// 스트림 병렬화 전
5761455
BUILD SUCCESSFUL in 2m 38s
// 스트림 병렬화 후
5761455
BUILD SUCCESSFUL in 40s

처음 20개의 메르센 소수를 생성하는 프로그램 👎

메르센 소수란 소수 가운데 2^n - 1로 표현되는 소수이다.

image

@Test
void generateMersenne() {
    primes().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);
}

만약 계산 속도를 높이고 싶어 스트림 파이프라인의 parallel()을 호출해버리면 프로그램은 응답 불가(liveness failure) 상태에 빠지게된다.

@Test
void generateMersenne() {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
            .filter(mersenne -> mersenne.isProbablePrime(50))
            .parallel() // 스트림 병렬화
            .limit(20)
            .forEach(System.out::println);
}

데이터 소스

parallel()은 병렬로 실행하기 위해 데이터 소스를 원하는 단위로 나눠서 스레드에 분배한다.

병렬화 ❌

  • Stream.iterate
    • 순차적으로 다음 요소를 반환해 원하는 단위로 자를 수 없다.

병렬화 ⭕️

  • ArrayList, HashMap, HashSet, ConcurrentHashMap, int, long
    • 데이터를 원하는 크기로 정확하고 쉽게 나눌 수 있다.
    • 참조 지역성(locality of reference) 이 뛰어나다. (= 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다.)
  • SplittableRandom
    • 랜덤한 수들로 이루어진 스트림을 병렬화 할때 사용한다.

중간 연산

병렬화 ❌

  • limit
    • 파이프라인 병렬화는 limit을 다룰 때, CPU 코어가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다.
    • 소수 찾기 처럼 점점 수행 시간이 길어지는 케이스의 경우 응답 불가 상태에 빠질 수 있다.

종단 연산

병렬화 ⭕️

  • 축소(reduction) : 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업
    • Stream의 reduce 혹은 min, max, count, sum
  • 조건에 맞으면 바로 반환되는 메서드
    • anyMatch, allMatch, noneMatch

병렬화 ❌

  • 가변 축소(mutable reduction)를 수행하는 Stream의 collect
    • 컬렉션들을 합치는 부담이 크다.

성능 점검 필요

  • 스트림 병렬화는 오직 성능 최적화 수단임을 기억하자!!
  • 병렬화에 드는 추가 비용이 있기 때문에 (스트림 안의 원소 수) * (원소당 수행되는 코드 줄 수) > 최소 수십만은 되어야 성능 향상을 기대할 수 있다.
  • 운영 환경과 유사한 조건에서 수행해보면서 성능 지표를 확인한 후 운영 코드에 최종 반영한다.

Ref.

728x90
댓글
공지사항
최근에 올라온 글