WeeklyStudy / modern-java-in-action

Modern Java in Action Study
0 stars 0 forks source link

스트림의 게으름(Lazy)이란 무엇이고 루프퓨전은 무엇인가? #10

Open ahah525 opened 1 year ago

ahah525 commented 1 year ago

문제

p.151에서 “스트림의 게으른 특성 덕분에 몇 가지 최적화 효과를 얻을 수 있었다” 고 하며 최적화 기법으로 쇼트서킷과 루프퓨전을 언급하고 있다.

스트림 특성인 게으름이 의미하는 바가 무엇인지 즉, lazy 방식이 eager 방식과 비교했을 때 무엇이 다른지를 이해하고 최적화 기법인 루프퓨전에 대해 알아보자.

참고로 쇼트서킷은 5장에서 자세히 설명한다고 나와있기 때문에 필요 시 예습 느낌으로 간단히 공부해보면 좋을 것 같아 질문에 포함시키진 않겠습니다.

관련 목차

참고자료

ahah525 commented 1 year ago

💡Eager Evaluation(조급한 연산)

변수에 접근하면 곧바로 연산을 시작하는 기법이다.

💡Lazy Evaluation(지연 연산)

결과값이 필요할 때까지 계산을 늦추는 기법이다.

  • 불필요한 연산은 수행하지 않기 때문에 대부분 성능이 좋다.
  • 다만, 지연 연산을 위한 내부 준비 작업을 수행하기 때문에 무조건 효율적인 방식은 아니다.

💡스트림의 지연 연산과 최적화

  1. 스트림 파이프라인이 실행된다.
  2. 전체 파이프라인 구성(어떤 중간 연산, 최종 연산으로 구성되었는지)를 검사한다.
  3. 검사 결과를 바탕으로 JVM은 어떤 방식으로 최적화를 진행할지 계획한다.
  4. 계획에 따라 최종 연산이 호출되는 시점에 스트림 연산을 수행한다.

💡루프 퓨전(loop fusion)이란?

파이프라인에서 복수의 스트림 연산을 하나의 연산 과정으로 병합하는 것이다.

  • 루프 퓨전은 스트림 최적화 전략의 일종이다.
  • 복수의 스트림 연산이 하나로 병합되면, 개별 스트림 요소에 접근하는 횟수가 최소화된다.
  • 다만, 모든 스트림 연산에 대해 루프 퓨전이 적용되는 것은 아니다!
  • 정렬 작업을 수행하기 위해서는 모든 요소가 필요하기 때문에 이러한 경우에는 루프 퓨전이 적용되지 않을 수 있다.

💡쇼트 서킷(short circuit)이란?

불필요한 연산을 수행하지 않음으로써 실행 속도를 높이는 기법이다.

  • 스트림 limit 같은 연산을 활용해 스트림 일부 요소들에 대한 연산을 완전히 생략한다.

💡Eager Evaluation 과 Lazy Evaluation 비교

1~10 이 담긴 리스트에서 1) 6보다 작고 2) 짝수인 요소에 3) 10을 곱하여 4) 리스트로 만드는 예제다.

1. Eager Evaluation

Eager Evaluation 방식의 반복문 예제

public static void t1() {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    int result = 0;
    // 1번
    List<Integer> list1 = new ArrayList<>();
    for (Integer i : list) {
        System.out.println(i + ": i < 6");
        if(i < 6) {
            list1.add(i);
        }
    }
    // 2번
    List<Integer> list2 = new ArrayList<>();
    for (Integer i : list1) {
        System.out.println(i + ": i % 2 == 0");
        if(i % 2 == 0) {
            list2.add(i);
        }
    }
    // 3번
    for (Integer i : list2) {
        System.out.println(i + ": i *= 10");
        i *= 10;
        result = i;
        break;
    }
    System.out.println(result);
}
  1. 전체 요소를 대상으로 6보다 작은지에 대한 검사를 반복한다.
  2. 1번에서 구한 요소를 대상으로 짝수인지에 대한 검사를 반복한다.
  3. 2번에서 구한 요소를 대상으로 10을 곱하는 것을 반복한다.

⇒ 한 요소에 3번 접근한다. 1, 2, 3 과정이 구분되어 있다.

Untitled (2)

2. Lazy Evaluation

Lazy Evaluation 방식의 Stream 예제

public static void t2() {
    final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    int result = list.stream()
            .filter(i -> {
                System.out.println(i + ": i < 6");
                return i < 6;
            })
            .filter(i -> {
                System.out.println(i + ": i % 2 == 0");
                return i % 2 == 0;
            })
            .map(i -> {
                System.out.println(i + ": i *= 10");
                return i * 10;
            })
            .findFirst()
            .get();
    System.out.println("================");
    System.out.println(result);
}
  1. 한 요소가 6보다 작은지 검사한다.
  2. 1번을 만족한다면 짝수인지 검사한다.
  3. 2번을 만족한다면 10을 곱한다.
  4. 3번에서 값을 구하면 나머지 요소에 대한 연산은 하지 않는다.

⇒ 한 요소에 1번 접근한다. 1, 2, 3 과정이 하나로 묶였다.

Untitled (3)

같은 동작을 하는 반복문

public static void t3() {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    int result = 0;
    // 1번
    for (Integer i : list) {
        System.out.println(i + ": i < 6");
        if(i<6) {
            System.out.println(i + ": i % 2 == 0");
            if(i%2==0) {
                System.out.println(i + ": i *= 10");
                i *= 10;
                result = i;
                break;
            }
        }
    }
    System.out.println("================");
    System.out.println(result);
}

💡결론

Reference