-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 아이템 48. 스트림 병렬화는 주의해서 적용하라 * 아이템 53. 가변인수는 신중히 사용하라 * Rename 스트림_병렬화는_주의해서_적용하라_켬미.md to 07장/아이템_48/스트림_병렬화는_주의해서_적용하라_켬미.md * Update 스트림_병렬화는_주의해서_적용하라_켬미.md * Update 스트림_병렬화는_주의해서_적용하라_켬미.md
- Loading branch information
Showing
2 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
# 자바에서 병렬 처리 방식 | ||
|
||
(자바 5부터) | ||
자바에 동시성 컬렉션 'java.util.concurrent' 라이브러리 | ||
실행자(Executor) 프레임워크 지원 | ||
|
||
(자바 7부터) | ||
고성능 병렬 분해(parallel decom-position) 프레임워크인 포크-조인(fork-join) 패키지 | ||
|
||
(자바 8부터) | ||
parallel 메서드만 한 번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원 | ||
|
||
<br> | ||
|
||
# 잘 사용해야겠죠? | ||
|
||
> 동시성 프로그래밍 할 때 안전성과 응답 가능 상태를 유지하기 위해 애써야 한다. | ||
```java | ||
public static void main(String [] args) { | ||
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) | ||
.filter(mersenne -> marsenne.isProbablePrime(50)) | ||
.limit(20) | ||
.forEach(Sysrem.out::println); | ||
} | ||
|
||
static Stream<BigInteger> primes() { | ||
return Stream.iterate(TWO, BigInteger::nextProbablePrime); | ||
} | ||
``` | ||
|
||
> 그냥 실행 : 12.5초 | ||
> 스트림 파이프라인의 parallel()을 호출해서 병렬처리 실행 : 응답 없음 진짜 오래 걸림 | ||
<br> | ||
|
||
### 스트림 라이브러리가 이 파이프라인을 병렬화로는 성능 개선을 기대할 수 없다. | ||
|
||
환경이 아무리 좋아도 데이터 소스가 `Stream.iterate`거나 중간 연산자로 `limit`를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다. | ||
|
||
<br> | ||
|
||
### Stream.iterate 는 왜? | ||
|
||
iterate 연산은 순서에 의존하는 연산이기 때문에 스트림 원소를 분할하기 어렵다. | ||
|
||
#### limit 왜? | ||
|
||
> 가정 : limit를 다룰 때 CPU 코어가 남는다면 원소를 몇개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다. | ||
이 코드의 경우 새롭게 메르센 소수를 찾을 때마다 그 전 소수를 찾을 때보다 두 배 정도 오래거린다. | ||
즉, 원소 하나를 계산한느 비용이 대략 그 이전까지의 원소 전부를 계산한 비용을 합친 것만큼 든다는 뜻 | ||
|
||
|
||
![](https://i.imgur.com/3raz8Jx.png) | ||
|
||
> 그래서 파이프라인은 자동 병렬화 알고리즘이 제 기능을 못하게 마비시키다. | ||
### 그럼 더 안좋아지고 이런데 어떻게 해? | ||
|
||
스트림 파이프라인을 마구잡이로 병렬화하면 안된다. | ||
|
||
**성능이 오히려 나빠질 수 있다.** | ||
|
||
<br> | ||
|
||
# 그럼 어떻게 사용하면 좋아요? | ||
|
||
<br> | ||
|
||
## 생성/중간 연산 좋은 예 | ||
|
||
- 스트림의 소스 : `ArrayList`, `HashMap`, `HashSet`, `ConcurrentHashMap`의 인스턴스 | ||
- 배열 범위 : int 범위, long 범위 | ||
|
||
**병렬화 효과 굿 !** | ||
|
||
<br> | ||
|
||
### 왜 좋은데? | ||
|
||
<br> | ||
|
||
#### 분배가 편해요 | ||
|
||
이 자료구조들은 모두 데이터를 **원하는 크기로 정확하고 손쉽게 나눌 수 있어**서 일을 **다수의 스레드에 분배하기에 좋다**는 특징이 있다. | ||
|
||
작업 나누기는 `Spliterator` 가 담당 | ||
- `Spliterator` 객체는 `Stream` 이나 `Iterable` 의 `spliterator` 메서드로 얻어올 수 있음 | ||
|
||
<br> | ||
|
||
#### 참조 지역성이 뛰어나요 | ||
|
||
**이웃한 원소의 참조들이 메모리에 연속**해서 **저장**되어 있다는 뜻 | ||
참조들이 가리키는 실제 객체가 메모리에서 서로 떨어져 있을 수 있는데, 그러면 참조 지역성이 나빠진다. | ||
|
||
![](https://i.imgur.com/E3ioJbu.png) | ||
|
||
|
||
참조 지역성이 낮으면 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 멍하니 보내게 된다. | ||
|
||
그래서 참조 지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화 할 때 아주 중요한 요소로 작용한다. | ||
|
||
<br> | ||
|
||
## 종단 연산 좋은 예 | ||
|
||
<br> | ||
|
||
### 축소(reduction) 이 접합하다 | ||
|
||
- Stream.reduce | ||
- min, max, count, sum (완성된 형태로 제공되는 메서드) | ||
- anyMatch, allMatch, noneMatch (조건에 맞으면 바로 반환되는 메서드) | ||
|
||
<br> | ||
|
||
### 가변 축소 (mutable reduction) 을 수행하는 stream의 collect 메서드는 병렬화에 적합하지 않다 | ||
|
||
Why? 컬렉션들을 합치는 부담이 크기 때문 | ||
|
||
|
||
> 직접 구현한 Stream, Iterable, Collection 이 병렬화의 이점을 제대로 누리게 하고 싶다면? | ||
> | ||
> - `spliterator` 메서드를 반드시 재정의하고 결과 스트림의 병렬화 성능을 강도 높게 테스트하라 | ||
<br> | ||
|
||
# 그러니까 주의하자 ~ | ||
|
||
### 성능 더 나빠지거나 결과가 잘못될 수 있음 | ||
|
||
스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다. | ||
|
||
> 안전 실패 : 결과가 잘못되거나 오동작하는 것 | ||
안전 실패는 병렬화한 파이프라인이 사용하는 mappers, filters, 혹은 프로그래머가 제공한 다른 함수 객체가 명세대로 동작하지 않을 때 벌어질 수 있다. | ||
Stream 명세는 이때 사용되는 함수 객체에 관한 엄중한 규약을 정의해놨다. | ||
|
||
Stream의 reduce 연산에 건네지는 accumulator(누적기) 와 combiner(결합기) 함수는 다음 요건을 따라야한다. | ||
|
||
- associative : 반드시 결합 법칙을 만족한다. | ||
- non-interfering : 간섭받지 않는다. | ||
- stateless : 상태를 갖지 않아야한다. | ||
|
||
|
||
물론 이 요구사항을 지키지 못하는 상태라도 파이프라인을 순차적으로만 수행한다면야 올바른 결과 GET | ||
|
||
BUT 요구사항도 안지키고, 병렬로 수행하면 결과 처참 ! | ||
|
||
|
||
그러니 위에 메르센 소수도 올바르게 출력하고 싶으면 종단 연산 `forEach`, `forEachOrdered`로 바꿔주면 된다. | ||
|
||
> -> 병렬 스트림들을 순회하며 소수를 발견한 순서대로 출력되도록 보장해줌 | ||
<br> | ||
|
||
### 성능 향상이 정말 되는지 확인해봐, 병렬화는 성능 최적화 수단일 뿐! | ||
|
||
파이프라인이 수행하는 진짜 작업이 병렬화에 드는 추가 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다. | ||
|
||
> 실제로 성능이 향상될지를 추정해보는 간단한 방법 | ||
> | ||
> 스트림 안에 원소 수와 원소당 수행되는 코드 줄 수를 곱해봐라. | ||
> (이 값이 최소 수십만은 되어야 성능 향상을 맛볼 수 있다.) | ||
#### 성능 테스트 하세요 | ||
|
||
다른 최적화와 마찬가지로 **변경 전 후 반드시 성능을 테스트**하여 병렬화를 사용할 가치가 잇는지 확인 해라 ! | ||
|
||
> 이상적으로는 운영 시스템과 흡사한 환경에서 테스트하는 것이 좋다 | ||
<br> | ||
|
||
#### 스레드 문제도 일으킬 수 있어요 | ||
|
||
보통 병렬 스트림 파이프라인도 공통의 포크-조인 풀에서 수행 (같은 스레드 풀 사용)되므로, 잘못된 파이프라인 하나가 시스템의 다른 부분의 성능에까지 악영향을 줄 수 있음을 유념하자 | ||
|
||
<br> | ||
|
||
# 그냥 안쓸까..? | ||
|
||
<br> | ||
|
||
### 그래도 돼 | ||
|
||
네 여러분이 스트림 파이프라인을 병렬화 할 일이 적습니다 ~ | ||
|
||
스트림을 많이 사용하는 수백만 줄짜리 코드를 여러 개 관리하다보면 그 중 스트림 병렬화가 효과가 많지 않음을 알게 됩니다 ! | ||
|
||
<br> | ||
|
||
### 그치만 좋을 때도 있긴해 ! | ||
|
||
**하지만** | ||
조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상을 만끽할 수도 있어요 ! | ||
|
||
> ex. 머신러닝, 데이터 처리 | ||
<br> | ||
|
||
#### 효과 굿 예시 | ||
|
||
> 소수 계산 스트림 파이프라인 | ||
|
||
```java | ||
static long pi(long n) { | ||
return LongStream.rangeClosed(2, n) | ||
.parallel() // 병렬 | ||
.mapToObj(BigInteger::valueOf) | ||
.filter(i -> i.isProbablePrime(50)) | ||
.count(); | ||
} | ||
``` | ||
|
||
|
||
> 저자 컴퓨터 (n = 100000000) | ||
> 원래 - 31초 | ||
> 병렬 - 9.2초 | ||
> 켬미 컴퓨터 (n = 100000000) | ||
> 원래 ![](https://i.imgur.com/EiiPMgm.png) | ||
> | ||
> 병렬 ![](https://i.imgur.com/Xd3X6Nm.png) | ||
|
||
<br> | ||
|
||
# 추가 꿀팁 | ||
|
||
무작위 수들로 이뤄진 스트림을 병렬화 하려거든 `ThreadLocalRandom` 보다는 `SplittableRandom` 인스턴스를 이용하자 | ||
|
||
> 만든 의도도 이렇다 | ||
> | ||
> `SplittableRandom` 은 멀티 스레드 -> 병렬화 하면 성능이 선형으로 증가한다. | ||
> `ThreadLocalRandom` 은 단일 스레드 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
|
||
# 가변 인수가 뭔데? | ||
|
||
```java | ||
public int sum(int... args) {} | ||
``` | ||
|
||
`원하는 타입` + `...` 을 사용하면 0개 이상의 인수를 유동적으로 입력 받을 수 있게 해주는 인수 | ||
|
||
> 그래서 인수 개수가 정해져 있지 않을 때 좋다! | ||
<br> | ||
|
||
# 어떻게 동작? | ||
|
||
인수의 개수와 길이가 같은 ==배열==을 만들고 인수들을 해당 배열에 저장하여 가변인수 메서드에 전 | ||
|
||
<br> | ||
|
||
# 문제 있어? | ||
|
||
<br> | ||
|
||
## 아니 동작 잘 되고 좋아~ | ||
|
||
```java | ||
static int sum(int... args) { | ||
int sum = 0; | ||
for(arg : args) { | ||
sum += arg; | ||
} | ||
return sum; | ||
} | ||
``` | ||
|
||
**Good !** | ||
|
||
<br> | ||
## 가끔 문제 있을 수도? | ||
|
||
```java | ||
static int min(int... args) { | ||
int min = Integer.MAX_VALUE; | ||
for(arg : args) { | ||
if(min > arg) { | ||
min = arg; | ||
} | ||
} | ||
|
||
return min; | ||
} | ||
``` | ||
|
||
**Bad !** | ||
|
||
인자가 0개 이면 가장 작은 값이 `Integer.MAX_VALUE` 인게 맞아? | ||
|
||
<br> | ||
|
||
### 인자가 0개 일 때, 또 따로 처리 해줘야 함 | ||
|
||
```java | ||
static int min(int... args) { | ||
if(args.length == 0) { | ||
throw new IllegalArgumentException("아무것도 없는데 최소를 어떻게 구해?"); | ||
} | ||
int min = args[0]; | ||
for(int i = 1; i < args.length; i++) { | ||
if(min > args[i]) { | ||
min = arg[i]; | ||
} | ||
} | ||
|
||
return min; | ||
} | ||
``` | ||
|
||
for-each 문도 사용 못하고 유효성 검사해야하고 귀찮다 | ||
|
||
그래서 인자가 0개이면 안되는 메서드는 다음과 같이 작성한다 | ||
|
||
```java | ||
static int min(int firstArg, int... args) { | ||
int min = firstArg; | ||
for(arg : args) { | ||
if(min > arg) { | ||
min = arg; | ||
} | ||
} | ||
|
||
return min; | ||
} | ||
``` | ||
|
||
<br> | ||
|
||
### 성능에 민감한 경우 가변인수는 걸림돌 | ||
|
||
가변인수는 메서드 호출 때 마다 배열을 할당하고 초기화 한다 | ||
|
||
#### 그래도 난 유동적이면서 성능은 지키고 싶어 ! | ||
|
||
자주 사용되는 개수까지는 값을 받고, 그 이상은 가변인수를 쓰자 | ||
|
||
```java | ||
public void foo() { } | ||
public void foo(int a1) { } | ||
public void foo(int a1, int a2) { } | ||
public void foo(int a1, int a2, int... rest) { } // 이거 사용되는 경우는 5%라고 하면 성능 최적화됨 | ||
``` | ||
|
||
특수한 상황에서는 이렇게 하면 사막의 오아시스처럼 성능 향상에 도움을 줌 | ||
|
||
> EnumSet 은 비트 필드를 대체하면서 성능까지 유지해야해서 위처럼 사용함 | ||