스트림 병렬화는 주의해서 적용하라
자바 8부터 parallel 메서드만 한 번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원한다.
이 병렬 실행을 올바르게 사용하는 방법을 알아보자.
import static java.math.BigInteger.ONE;
import static java.math.BigInteger.TWO;
import java.math.BigInteger;
import java.util.stream.Stream;
// 스트림을 사용해 처음 20개의 메르센 소수를 생성하는 프로그램
public class Main {
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);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
위의 코드는 메르센 소수를 생성하는 프로그램이다.
실행 결과는 20개의 메르센 소수를 출력한다.
이 코드의 성능을 높이기 위해서 아래처럼 스트림 파이프라인의 parallel()을 호출하면 성능이 좋아질까?
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.parallel()
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
위처럼 parallel을 추가해도 아무런 일이 발생하지 않는다. (아무것도 출력되지 않음)
스트림 라이브러리가 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문
환경이 아무리 좋더라도 데이터 소스가 Stream.iterate 거나
중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
1. Stream.iterate
parallel은 병렬로 실행하기 위해 데이터를 특정 단위로 자르는데,
iterate는 순차적으로 다음 요소를 반환하기 때문에 데이터 소스를 잘라서 여러 스레드로 나눌 수 없다.
따라서 parallel이 병렬화하는 방법을 찾지 못한다.
2. limit 사용 시 병렬화 성능 개선을 얻지 못하는 이유:
위의 코드가 쿼드 코어 시스템에서 동작한다고 하자.
마지막 20번째 계산이 수행될 시점에 나머지 3개의 cpu는 놀고 있을 것이다.
따라서 놀고 있는 cpu들에서 21, 22, 23번째 메르센 소수를 구하는 작업을 하는데,
20번째 메르센 소수를 구해도 나머지 cpu들이 작업을 진행하고 있기 때문에,
작업 시간이 늘어나게 된다.
( cpu 코어가 남는다면 원소 몇 개를 더 처리 하고, 제한된 개수 이후의 결과를 버림 )
병렬화하기 좋은 자료구조
- ArrayList
- HashMap
- HashSet
- ConcurrentHashMap
- 배열
- int 범위
- long 범위
위의 자료구조들은 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있기 때문에
일을 다수에 스레드에 분배하기 좋다는 특징이 있다.
그리고 참조 지역성이 좋다는 특징이 있다.
참조 지역성이란?
이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있는 것
메모리에 연속해서 저장되어있으면 다음 참조에 대한 접근 속도가 빠르다.
예를 들어 LinkedList는 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있지 않는다.
따라서 참조 지역성이 낮다.
참조 지역성과 병렬화와의 관계
참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 버리게 된다.
따라서 참조 지역성이 좋은 자료구조를 이용해 병렬화를 하자.
스트림 종단 연산
스트림의 종단 연산 또한 병렬화에 영향을 준다.
종단 연산 중 병렬화에 가장 적합한 것은 축소 ( 모든 원소를 하나로 합치는 작업) 이다.
- min
- max
- sum
- count
조건에 맞으면 바로 반환되는 메서드도 병렬화에 적합하다.
- anyMatch
- allMatch
- noneMatch
가변 축소는 병렬화에 적합하지 않다. 컬렉션들을 합치는 부담이 크기 때문
결론
스트림은 잘못 병렬화하면 성능이 오히려 안좋아질 수 있다.
-> 모르면 안하는게 좋다.
스트림 병렬화는 오직 성능 최적화 수단이다.
즉, 성능 테스트를 통해 병렬화를 사용할 가치가 있는 지 확인해야한다.
스트림 병렬화가 효과를 보는 경우는 많지 않다.
성능이 좋아졌음이 확실해졌을 때, 그럴 때만 병렬화를 사용해라 !!!!
혼자 삽질해본 코드
double beforeTime = System.currentTimeMillis(); //코드 실행 전에 시간 받아오기
LongStream.rangeClosed(1, 5000000)
.parallel()
.forEach(System.out::println);
double afterTime = System.currentTimeMillis(); // 코드 실행 후에 시간 받아오기
double secDiffTime = (afterTime - beforeTime)/1000; //두 시간에 차 계산
System.out.println("시간차이(m) : "+ secDiffTime);
1 부터 5000000까지 출력하는 코드
parallel 사용 시 : 16초
parallel() 없을 때 : 12초
1부터 10000000까지 출력하는 코드
parallel 사용 시 : 31초
parallel() 없을 때 : 23초
1부터 50000000까지 출력하는 코드
parallel 사용 시 : 192초
parallel() 없을 때 : 121초
parrallel 사용 시 더 오래걸리는것을 확인할 수 있었다.
병렬화 제대로 알고 쓰자