스트림을 사용하면 내부 반복으로 네이티브 자바 라이브러리가 스트림 요소의 처리를 제어할 수 있다. 따라서 개발자는 컬렉션 데이터 처리 속도를 높이려고 따로 고민할 필요가 없다. 컴퓨터의 멀티코어를 활용해서 파이프라인 연산을 실행할 수 있다는 점이 중요한 특징이다.
7.1 병렬 스트림
- 컬렉션에 parallelStream을 호출하면 병렬 스트림이 생성된다.
- 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다.
- 따라서 모든 멀티코어 프로세서가 각각의 청크 처리를 할당할 수 있다.
병렬 실행이 무조건 빠른 것은 아니다.
static long sum(int n) {
long sum = 0;
for (int i = 0; i < n; i++) {
sum += 1;
}
return sum;
}
static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, Long::sum);
}
static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}
- 이 세 함수에 대해서 일반 반복 > 순차 스트림 > 병렬 스트림 순으로 속도가 빨랐다.
- 문제점
- 반복 결과로 박싱된 객체가 만들어지므로 숫자를 더하려면 언박싱을 해야 한다.
- 반복 작업은 병렬로 수행될 수 있는 독립 단위로 나누기가 어렵다.
- 위의 iterate 연산은 이전 연산의 결과에 따라 다음 함수의 입력이 달라지기 때문에 청크로 분할하기 어려운 것이다.
- 리듀싱 과정을 시작하는 시점에 전체 숫자 리스트가 준비되지 않았기 때문에 분할할 수 없는 것
- 결국 순차 처리와 다를 게 없는데 스레드 할당 오버헤드만 증가
특화된 메서드 사용 LongStream.rangeClosed
static long sequentialSum(long n) {
return LongStream.rangeClosed(1L, n)
.reduce(0L, Long::sum);
}
static long parallelSum(long n) {
return LongStream.rangeClosed(1L, n)
.parallel()
.reduce(0L, Long::sum);
}
- LongStream.rangeColsed는 기본형을 직접 사용하므로 언박싱 오버헤드가 사라진다.
- LongStream.rangeColsed는 쉽게 청크로 분할할 수 있는 숫자 범위를 생산한다.
- 결과로 일반 반복 > 병렬 스트림 > 순차 스트림 순으로 빨랐다.
-> 올바른 자료구조를 선택해야 병렬 실행도 최적의 성능을 발휘할 수 있다.
병렬화가 공짜는 아니다.
- 스트림을 재귀적으로 분할
- 각 서브 스트림을 서로 다른 스레드의 리듀싱 연산으로 할당
- 이들 결과를 하나의 값으로 합치기
- 멀티 코어간 이동은 생각보다 비싸다.
- 따라서 코어간 데이터 전송보다 훨씬 오래 걸리는 작업만 병렬로 수행하는 것이 바람직
병렬 스트림은 올바르게 동작하려면 공유된 가변 상태를 피해야 한다.
static long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;
}
static class Accumulator {
public long total = 0;
public void add(long value) {
total += value;
}
}
- total에 접근할 때마다 (다수의 스레드에서 동시에 데이터에 접근하는) 데이터 레이스 문제가 일어난다.
- 성능은 둘째 치고 올바른 결괏값이 나오지 않는다.
병렬 스트림 효과적으로 사용하기
- 확신이 서지 않으면 직접 측정하라
- 박싱을 주의하라 (기본형 특화 스트림을 사용하자)
- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산이 있다.
- limit나 findFirst처럼 순서 의존 연산은 병렬에서 비싼 비용을 치른다.
- findAny가 더 성능이 좋다.
- 정렬된 스트림에 unordered를 호출하면 비 정렬된 스트림을 얻을 수 있다. 요소 순서가 상관없다면 비정렬 스트림에 limit를 호출하는 것이 더 효율적이다.
- 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하라.
- 처리 요소 수가 N이고 요소당 처리 비용이 Q일 때 총 처리 비용 = N*Q
- Q가 높아진다는 것은 병렬 스트림으로 성능 개선 가능성이 있다.
- 소량의 데이터에서는 병렬 스트림이 도움이 되지 않는다.
- 스트림을 구성하는 자료구조가 적절한지 확인하라
- LinkedList보다 ArrayList가 분해가 효율적이다.
- range 팩토리 메서드로 만든 기본형 스트림도 쉽게 분해할 수 있다.
- 커스텀 Spliterator를 구현해서 분해 과정 완벽 제어 가능
- 스트림의 특성과 중간 연산이 스트림을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있다.
- 필터 연산이 있으면 스트림 길이 예측이 어렵기 때문에 효과적으로 병렬 처리 가능한지 알 수 없게 된다.
- 최종 연산의 병합 과정 비용을 살펴보라.
- 병합 비용이 비싸다면 병렬로 얻은 성능 이익이 상쇄될 수 있다.
- 병렬 스트림이 수행되는 내부 인프라 구조도 살펴봐야 한다.
- 포크/조인 프레임워크
'개발 서적 > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] Chapter13. 디폴트 메서드 (0) | 2022.04.05 |
---|---|
[모던 자바 인 액션] Chapter11. Null 대신 Optional 클래스 (0) | 2022.03.28 |
[모던 자바 인 액션] Chapter4. 스트림 소개 (0) | 2022.02.28 |