사실 이 모든 시작은 IntStream이었습니다.
전통적 for loop 가 IntStream 보다 가독성도 좋고
성능도 더 뛰어날 거라고 생각해서 실험을 해보게 되었는데 그만...
😡 for loop가 근본이라구우!!
사람은 자기가 먼저 알고 있던 정보를 더 신뢰하고자 하는 경향이 있다고 합니다...
그래서 for-loop를 IntStream 보다 위에 보여드립니다 흐흐흐..
List<Integer> lottoNumbersWithForLoop = new ArrayList<>();
for (int i = 1; i <= 45; i++) {
lottoNumbersWithForLoop.add(i);
}
List<Integer> lottoNumbersWithStream =
IntStream.rangeClosed(1, 46)
.boxed()
.collect(toList());
위의 두 코드 중 저는 위의 코드가 더 낫다고 생각했습니다. (매직 넘버는 잠시 잊어주세요! ㅋㅋ)
새로운 것을 받아들이기 싫어하는 관성도 분명 있었을 거고요, (이게 크긴 합니다;;)
성능적으로도 전통적 for-loop가 나을 거라 판단했습니다.
가독성은 사바사지만 저는 이것도 for-loop가 나을 거라 생각했었어요.
그래서 제 생각이 맞다는 근거를 찾아 나섰습니다..
🤔
🏃 for-loop vs IntStream 성능 비교
for-loop야 힘내 !!
public static final int TEN_MILLION = 10_000_000;
public static final int MILLION = 1_000_000;
public static final int HUNDRED = 100;
@ParameterizedTest(name = "시행 횟수 : {0}")
@ValueSource(ints = {HUNDRED, MILLION, TEN_MILLION})
@DisplayName("for-loop, IntStream 성능 비교")
void forLoopVersusIntStream (int times) {
// for-loop 로 생성
Supplier<List<Integer>> usingForLoop = () -> {
List<Integer> lottoNumbersWithForLoop = new ArrayList<>();
for (int i = 1; i <= 45; i++) {
lottoNumbersWithForLoop.add(i);
}
return lottoNumbersWithForLoop;
};
// IntStrearm 으로 생성
Supplier<List<Integer>> usingIntStream = () -> {
return IntStream.rangeClosed(1, 46)
.boxed()
.collect(toList());
};
// for-loop 로 반복수행
BiFunction<Supplier<List<Integer>>, Integer, Long> runner = (supplier, runTimes) -> {
long started = System.currentTimeMillis();
for (int i = 0; i < runTimes; i++) {
List<Integer> integers = supplier.get();
}
long finished = System.currentTimeMillis();
return finished - started;
};
// IntStream 으로 반복 수행
BiFunction<Supplier<List<Integer>>, Integer, Long> runner = (supplier, runTimes) -> {
long started = System.currentTimeMillis();
IntStream.rangeClosed(0, runTimes)
.parallel() // 병렬 적용 또는 미적용
.forEach((i) -> supplier.get());
long finished = System.currentTimeMillis();
return finished - started;
};
System.out.printf("for-loop took %dms for %dtimes%n", runner.apply(usingForLoop, times), times);
System.out.printf("IntStream took %dms for %dtimes%n", runner.apply(usingIntStream, times), times);
}
실험할 코드는 위와 같습니다.
1에서 45까지의 숫자를 담은 List<Integer>를 생성하는 것을 1회로 정의합니다.
이 1에서 45까지 담는 1회의 작업을 for-loop로 하는지, IntStream으로 하는지의 선택이 가능하고,
이를 100번, 100만번, 1000만번 반복할 때 for-loop로 하는지, IntStream으로 하는지의 선택이 가능합니다.
아래 표와 사진은 실험 결과입니다.
for-loop으로 생성 (ms) | IntStream으로 생성 (ms) | 비고 | |
for-loop로 100번 반복 | 0 | 5 | |
for-loop로 100만번 반복 | 534 | 1162 | 가장 느림 |
for-loop로 1000만번 반복 | 5765 | 8149 | 가장 느림 |
IntStream으로 100번 반복 | 0 | 0 | |
IntStream으로 100만번 반복 | 320 | 997 | |
IntStream으로 1000만번 반복 | 2169 | 6102 | |
IntStream으로 100번 반복 | 11 | 3 | 병렬 적용 |
IntStream으로 100만번 반복 | 165 | 496 | 병렬 적용 |
IntStream으로 1000만번 반복 | 784 | 2235 | 병렬 적용 |
결과를 해석해보자면..
반복 횟수가 적은 작업에 대해서는 for-loop가 유리하고
반복 횟수가 많아질수록 stream을 사용하는 게 유리하다고 판단됩니다.
그래서 반복 횟수가 적은 작업을 여러회 돌릴 경우,
반복 횟수가 적은 작업 자체는 for-loop으로 선언하되,
그것을 여러회 돌리는 선언은 Stream을 사용하는 것이 가장 효과적이라고 하겠습니다.
만약 스레드 세이프한 작업이라면 parallel() 선언을 통한 병렬 처리 시 가장 성능적으로 우수합니다.
💸 로또의 양적 완화(?)
로또를 마구 찍어내 봅시다!!
IntStream.rangeClosed(0, 1_000)
.parallel()
.mapToObj(i -> LottoFactory.createAutoLottosByQuantity(10))
.collect(toList());
앞서 리스트에 1에서 45까지의 숫자를 담는 테스트를 해봤으니,
이번에 로또를 신나게 발행해보려 했습니다.
로또를 10 게임씩 1000번 찍어내보려고 했는데!
로또 숫자 6개를 List로 갖는 List<LottoNumber>를 new Lotto() 생성자의 매개 변수로 전달하는데,
이때 중복된 숫자가 전달된 것 같습니다.
private static final List<Integer> lottoNumbers;
private static final int MINIMUM_LOTTO_NUMBER = 1;
private static final int MAXIMUM_LOTTO_NUMBER = 45;
static {
lottoNumbers = new ArrayList<>();
for (int i = MINIMUM_LOTTO_NUMBER; i <= MAXIMUM_LOTTO_NUMBER; i++) {
lottoNumbers.add(i);
}
}
private static List<LottoNumber> getLottoNumbers() {
Collections.shuffle(lottoNumbers);
return lottoNumbers.stream()
.limit(6)
.sorted()
.map(LottoNumberRepository::getLottoNumberByInt)
.collect(toList());
}
문제의 코드는 shuffle 이었습니다.
static으로 선언된 lottoNumbers를 멀티 스레드가 우당탕 달려들어서
남이 숫자 꺼내고 있는데 섞어버리고 하다보니 ;;;; 중복된 숫자가 전달되게 된 거죠.
제가 작성한 코드가 멀티 스레드 환경에서 이렇게 처참히 박살나는 걸 직접 경험하고 나니
컨트롤러에 상태값이 없어야 한다 라는 말이 절로 이해가 됐습니다.
성능을 위해선 당연히 멀티스레드 환경에도 안전해야 할텐데..
그런 것까지 고려하려면 정말 더 많은 기반지식이 필요하겠구나 싶습니다.
간단하게 synchronized 를 붙이면 일단 문제 없이 돌아는 가지만,
병렬처리의 성능상 이점은 모두 포기하는 것이기에 상당히 허탈합니다.
StringBuffer vs StringBuilder
StringBuffer 는 synchronized 지옥입니다...
As of release JDK 5, this class has been supplemented with an equivalent class
designed for use by a single thread StringBuilder.
The StringBuilder class should generally be used in preference to this one,
as it supports all of the same operations but it is faster, as it performs no synchronization.
StringBuffer 는 JDK 1.0부터, StringBuilder 는 JDK 1.5부터입니다.
StringBuffer 는 멀티 스레드 환경에서 저의 코드처럼 박살나지 않을 수 있게 설계됐습니다.
synchronized 를 이용해서 말이죠.
그러나 JavaDoc에도 작성되어 있듯이, StringBuffer 의 모든 기능을 제공하고,
synchronized 가 제거되어 더 빠른 성능을 보장하는 StringBuilder 사용을 권장하고 있습니다.
문자열이 공유되는 상태값으로 존재하고, 이를 동시에 접근하여 수정하는 일이
과연 존재할까 라는 생각이 들고, 만약 존재한다면 그때 StringBuffer를 쓰면 된다....
라고 정리해야할 것 같지만 그냥 StringBuffer를 잊어도 되지 않을까.. 하는 생각을 해봅니다..
🤣
for-loop과 IntStream의 성능 비교,
멀티 스레드 환경에서 공유되는 자원의 상태 변경,
StringBuffer와 StringBuilder 에 대해 끄적여봤습니다 ㅎ
'우아한테크코스 4기' 카테고리의 다른 글
🏎️자동차 경주와 💸로또에서 배운 것들 (2) | 2022.03.08 |
---|---|
컬렉션의 복사 방법을 정리해봅시다! (unmodifiable view / list) (6) | 2022.02.28 |
EnumMap, 니가 그렇게 빨라?? (4) | 2022.02.26 |
첫 오프라인 페어 프로그래밍! (6) | 2022.02.24 |
함께 자라기 - 우아한테크코스에 적용된 애자일 (0) | 2022.02.21 |