이펙티브 자바, 쉽게 정리하기 - item 48. 스트림 병렬화는 주의해서 적용하라
자바 언어와 동시성
- 동시성 프로그래밍에서는 항상 앞서가있었다.
- 1996년부터 스레드, 동기화, wait/notify를 지원
- 자바 5부터
java.util.concurrent
,Executor
등을 선도적으로 지원했다. - 자바 7부터 fork/join 패키지를 추가
- 자바 8부터 병렬 스트림을 지원
- 스트림에서는
parallel()
을 통해 손쉽게 동시성을 제공했다.
동시성 주의점
- 안전성(safety) 과 응답 가능(liveness)
메르센 소수 구하기 예제로 parallel()
문제 살펴보기
메르센 소수란 2의 n승 빼기 1로 표현되는 소수를 말한다.
@Test
public void mersenne() {
primes()
.map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
- 메르센 소수의 20번째까지 구하는 메서드이다.
- 총 소요시간으로
5356 ms
가 출력됐다.
@Test
public void mersenne() {
primes()
.parallel()
.map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
- 병렬 연산을 이용해 퍼포먼스를 낫게 해보고 싶어
parallel()
을 붙인다.- 그러나 이 코드 때문에 되려 연산이 끝나지 않는다.
- 자바는 파이프라인을 병렬화할 방법을 찾아내지 못했기 때문이다.
- 데이터 소스가
Stream.iterate
거나 중간 연산으로limit
을 쓰면 병렬화로 성능 개선을 기대할 수 없다.- 파이프라인 병렬화는
limit
이 있을 때, CPU 코어가 남는다면 원소를 몇개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다. - 계속 버려지기 때문에 계속 이전까지의 값을 다시 구해야 한다.
- 파이프라인 병렬화는
스트림 파이프라인을 마구잡이로 병렬화하면 성능이 오히려 끔찍하게 나빠질 수 있다.
병렬화(parallel()
)를 적용하는 기준
- 스트림의 소스가
ArrayList
,HashMap
,HashSet
,ConcurrentHashMap
의 인스턴스거나, 배열,int
범위,long
범위 등 쪼개기 쉬울 때 병렬화의 효과가 가장 좋다.- 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 스레드에 분배하기 좋다.
- 위 자료구조는 참조 지역성이 뛰어나다. 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다. 이 경우 성능이 좋아진다.
- 기본타입 배열이 참조 지역성이 제일 좋다.
- 참조 지역성이 낮으면, 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 시간을 보내게 된다.
종단 연산과 병렬 수행 효율
- 스트림 파이프라인의 종단 연산의 동작방식 역시 병렬 수행 효율에 영향을 준다.
- 병렬 연산에 가장 적합한 것은 축소(
reduction
)이다. - 이도 역시 같은 원리를 따라서 쪼개기 쉬울수록 병렬화의 효과가 좋다.
- 특정 집계 연산, 조건에 맞으면 반환하는 등의 메서드 (
min
,max
,count
,sum
) 는 병렬화하기 좋다. - 조건에 맞으면 바로 반환하는 메서드들 (
anyMatch
,allMAtch
,noneMatch
) 도 적합하다.
- 특정 집계 연산, 조건에 맞으면 반환하는 등의 메서드 (
- 병렬 연산에 가장 적합한 것은 축소(
spliterator()
를 재정의하고 스트림의 병렬화 성능을 강도높게 테스트 후에 병렬화를 적용해야 한다.- 조건에 맞으면
parallel()
호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상을 맛볼 수 있다.
스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다. 결과가 잘못되거나 오동작하는 것을
안전 실패 (safety failure)
라고 한다.
안전 실패가 일어나지 않게 만들기 위해서는
Stream
의 명세를 잘 지켜야 한다. 이를테면accumulator
와combiner
함수는 반드시 결합 법칙을 지켜야 하며, 간섭받지 않아야 하고, 상태를 갖지 않아야 한다.
스트림 병렬화가 효과를 보는 경우의 코드: 2~n까지의 소수 구하기
public long pi(long n) {
return LongStream.range(2, n)
.parallel() // 1 sec 412 ms
// 8 sec 192 ms, 약 5.81배 성능 향상
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
- 위 작업은 쪼개기 쉬우므로,
parallel()
메서드가 효과가 있다.- 숫자들을 일정부분씩 쪼개
isProbablePrime()
의 결과에 따라 나누면 된다.
- 숫자들을 일정부분씩 쪼개
무작위 수로 이뤄진 스트림을 병렬화하고 싶다면
ThreadLocalRandom
혹은Random
보다는SplittableRandom
인스턴스를 이용하는 것이 좋다. 처음부터SplittableRandom
인스턴스의 목적은 이것이었다.그냥
Random
의 경우 모든 연산을 동기화하기 때문에 병렬 처리하면 최악의 성능을 보일 수 있다.
핵심 정리
- 무작정 병렬화를 한다고 속도가 빨라질 것이라 생각하지 말자.
- 병렬화 시 오동작 등의 부작용도 항상 고려해야 한다.
- 성능지표를 항상 유심히 관찰하자.
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 50. 적시에 방어적 복사본을 만들라 (0) | 2023.06.07 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 49. 매개변수가 유효한지 검사하라 (0) | 2023.06.02 |
이펙티브 자바, 쉽게 정리하기 - item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2023.03.31 |
이펙티브 자바, 쉽게 정리하기 - item 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2023.03.30 |
이펙티브 자바, 쉽게 정리하기 - item 44. 표준 함수형 인터페이스를 사용하라 (0) | 2023.03.29 |