Skip to content

Latest commit

 

History

History
82 lines (58 loc) · 5.29 KB

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

File metadata and controls

82 lines (58 loc) · 5.29 KB

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

  • 자바로 동시성 프로그램을 작성하기가 점점 쉬워지고는 있지만, 이를 올바르고 빠르게 작성하는 일은 여전히 어려운 작업이다.
  • 동시성 프로그래밍을 할 때는 안전성과 응답 가능 상태를 유지하기 위해 애써야 한다. (병렬 스트림 파이프라인 프로그래밍에서도 마찬가지)

스트림 파이프라인을 마구잡이로 병렬화하면 안 된다. 성능이 오히려 끔찍하게 나빠질 수도 있다.

(아이템 45에서 다루었던) 메르센 소수 생성 프로그램

public static void main(String[] args) {
	primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
		.filter(mersenne -> mersenne.isProbablePrime(50))
		.limit(20)
		.forEach(System.out::println);
}

public Stream<BigInteger> primes() {
	return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
  • 스트림 파이프라인의 parallel()을 호출하면 프로그램은 아무것도 출력하지 못하면서 CPU는 90%나 잡아먹는 상태가 무한히 계속된다.
  • 프로그램이 이렇게 느려진 원인은 스트림 라이브러리가 이 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다.
  • 데이터 소스가 Stream.iterate거나 중간 연산으로 limit을 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
  • 파이프라인 병렬화는 limit을 다룰 때 CPU 코어가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 된다고 가정한다.

스트림 병렬화 주의점

스트림의 소스가 ArrayList, HashMap/Set, ConcurrentHashMap의 인스턴스거나 배열, int/long 범위일 때 병렬화의 효과가 가장 좋다.

  • 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기에 좋다.

    TIPS: 나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream이나 Iterable의 spliterator 메서드로 얻어올 수 있다.

  • 원소들을 순차적으로 실행할 때의 참조 지역성(이웃한 원소의 참조들이 메모리에 연속해서 저장)이 뛰어나다.


스트림 파이프라인의 종단 연산의 동작 방식 역시 병렬 수행 효율에 영향을 준다.

  • 종단 연산 중 병렬화에 가장 적합한 것은 축소다. (예. Stream의 reduce, 혹은 min, max, count, sum 메서드)
  • 조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다. (예. anyMatch, allMatch, noneMatch)
  • 가변 축소를 수행하는 메서드는 컬렉션들을 합치는 부담이 크기 때문에 병렬화에 적합하지 않다. (예. Stream의 collect 메서드)

스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다.(안전 실패)

  • 병렬화한 파이프라인이 사용하는 mappers, filters, 혹은 프로그래머가 제공한 다른 함수 객체가 명세대로 동작하지 않을 때 벌어질 수 있다.
  • Stream 명세는 이때 사용되는 함수 객체에 관한 엄중한 규약을 정의해놨다.
    • reduce 연산에 건네지는 accumulator와 combiner 함수는 반드시 결합법칙을 만족하고, 간섭받지 않고, 상태를 갖지 않아야 한다.

TIPS: 출력 순서를 순차 버전처럼 정렬하고 싶다면 종단 연산 forEach를 forEachOrdered로 바꿔주면 된다.


심지어 데이터 소스 스트림이 효율적으로 나눠지고, 병렬화하거나 빨리 끝나는 종단 연산을 사용하고, 함수 객체들도 간섭하지 않더라도,
파이프라인이 수행하는 진짜 작업이 병렬화에 드는 추가 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다.

  • 간단한 성능 추정 방법: 스트림 안의 원소 수*원소당 수행되는 코드 줄 수가 최소 수십만은 되어야 성능 향상을 맛볼 수 있다.

변경 전후로 반드시 성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다. 보통은 병렬 스트림 파이프라인도 공통의 포크-조인 풀에서 수행되므로(즉, 같은 스레드 풀을 사용하므로), 잘못된 파이프라인 하나가 시스템의 다른 부분의 성능에까지 악영향을 줄 수 있음을 유념하자.


스트림 파이프라인을 병렬화할 일이 적어질 것 같은데?

  • 적어질 것처럼 느껴졌다면, 그건 진짜 그렇기 때문이다. 스트림 병렬화가 효과를 보는 경우가 많지 않다.
  • 그렇다고 스트림을 병렬화하지 말라는 뜻은 아니다.
  • 조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상을 만끽할 수 있다.

(n보다 작거나 같은) 소수 계산 스트림 파이프라인 - 병렬화 버전

static long pi(long n) { 
    return LongStream.rangeClosed(2, n)  
		    .parallel()
            .mapToObj(BigInteger::valueOf)  
            .filter(i -> i.isProbablePrime(50))  
            .count();  
}

TIPS: 무작위 수들로 이뤄진 스트림을 병렬화하려거든 ThreadLocalRandom, Random보다는 SplittableRandom 인스턴스를 이용하자.