7장. 병렬 데이터 처리와 성능
컬렉션에 parallelStream을 호출하면 병렬 스트림이 생성된다.
public long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, Long::sum);
}
n이 커진다면 병렬로 처리하는 것이 좋을텐데 한 번 처리해보자.
public long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}
순차 스트림에 parallel()
메서드를 호출하면 기존의 함수형 리듀싱 연산이 병렬로 처리된다.
순차 스트림에 parallel()
을 호출해도 스트림 자체에는 아무 변화도 일어나지 않는다. 내부적으로 parallel을 호출하면 이후 연산이 병렬로 수행해야 함을 의미하는 불리언 플래그가 설정된다. 반대로 sequential로 병렬 스트림을 순차 스트림으로 바꿀 수 있다.
stream.parallel()
.filter(...)
.sequential()
.map(...)
.parallel()
.reduce();
parallel과 sequential 두 메서드 중 최종적으로 호출된 메서드가 전체 파이프라인에 영향을 미친다.
위의 경우에는 parallel이므로 파이프라인은 전체적으로 병렬로 실행된다.
LongStream.rangeClosed
메서드는 iterate에 비해 다음과 같은 장점을 제공한다.
- 기본형 long을 직접 사용하므로 박싱과 언박싱 오버헤드가 사라진다.
LongStream.rangeClosed
는 쉽게 청크로 분할할 수 있는 숫자 범위를 생산한다. 예를 들어 1 ~ 20 범위의 숫자를 1 ~ 5, 6 ~ 10, 11 ~ 15, 16 ~ 20 범위의 숫자로 분할할 수 있다.
public long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).forEach(accumulator::add);
return accumulator.total;
}
public class Accumulator {
public long total = 0;
public void add(long value) {
total += value;
}
}
병렬 스트림을 잘못 사용하면서 발생하는 많은 문제는 공유된 상태를 바꾸는 알고리즘을 사용하기 때문에 일어난다.
위 코드는 본질적으로 순차 실행할 수 있도록 구현되어 있으므로 병렬로 실행하면 참사가 일어난다. 특히 total을 접근할 때마다(다수의 스레드에서 동시에 데이터에 접근하는) 데이터 레이스 문제가 일어난다. 동기화로 문제를 해결하다보면 결국 병렬화라는 특성이 없어져 버릴 것이다.
public long sideEffectParallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;
}
첫 번째 코드로 실행하면 여러 스레드에서 동시에 누적자, 즉 total += value를 실행하면서 이런 문제가 발생한다. 얼핏 보면 아토믹 연산 같지만 total += value는 아토믹 연산이 아니다. 결국 여러 스레드에서 공유하는 객체의 상태를 바꾸는 forEach 블록 내부에서 add 메서드를 호출하면서 이 같은 문제가 발생한다.
- 확신이 서지 않으면 직접 측정하라. 병렬 스트림이 순차 스트림보다 빠른 것은 아니기 때문에 애매하면 직접 성능을 측정하자
- 박싱을 주의해라. 자동 박싱과 언박싱은 성능을 크게 저하시킬 수 있는 요소다. 기본형 특화 스트림을 사용하자
- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산이 있다. 특히 limit나 findFirst처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하려면 비싼 비용을 치뤄야 한다. 스트림에 N개 요소가 있을 때 요소의 순서가 상관없다면 비정렬된 스트림에 limit를 호출하는 것이 더 효율적이다
- 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하라
- 소량의 데이터에서는 병렬 스트림이 도움 되지 않는다. 소량의 데이터를 처리하는 상황에서는 병렬화 과정에서 생기는 부가 비용을 상쇄할 수 있을 만큼의 이득을 얻지 못하기 때문이다
- 스트림을 구성하는 자료구조가 적절한지 확인하라
- 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있다
- 최종 연산의 병합 과정 비용을 살펴보라
'공부 기록들 > 우테코' 카테고리의 다른 글
우아한 테크코스 한 달 생활기 (1) | 2021.04.09 |
---|---|
[모던 자바 인 액션 스터디] 8장 컬렉션 API 개선 (0) | 2021.03.28 |
[모던 자바 인 액션 스터디] 6장 스트림 활용 (0) | 2021.03.21 |
[모던 자바 인 액션 스터디] 5장 스트림 활용 (0) | 2021.03.14 |
[모던 자바 인 액션 스터디] 4장 스트림 소개 (0) | 2021.03.14 |