Java Stream 중간연산 호출 회수에 따른 성능 차이

Sangwoo Park·2023년 1월 23일
0

궁금증

어느날 코딩을 하다가 Stream 의 중간연산에 대한 궁금증이 떠올랐다. 아래는 테스트를 위해 만들어본 예시 코드이다.

(1) 중간연산을 여러번에 걸쳐서 하는 경우

IntStream.range(1, trial)
                .filter(x -> x % 2 == 0)
                .filter(x -> x % 3 == 0)
                .filter(x -> x % 5 == 0)
                .filter(x -> x % 7 == 0)
                .filter(x -> x % 11 == 0)
                .boxed()
                .collect(Collectors.toList());

(2) 중간연산 하나에 해결하는 경우

IntStream.range(1, trial)
                .filter(x -> x % 2 == 0
                        && x % 3 == 0
                        && x % 5 == 0
                        && x % 7 == 0
                        && x % 11 == 0)
                .boxed()
                .collect(Collectors.toList());

위 두가지 경우에 성능 차이가 있을까?

가설

1번은 가독성이 좋아보이지만, 중간연산을 여러번 하게 되어, 2번에 비해 성능이 떨어질 것이다.
2번은 코드가 더 복잡해진다면 가독성이 떨어질 수 있지만, 한 번의 중간연산만 하므로 1번에 비해 성능이 좋을 것이다.

테스트

테스트 코드를 만들어 1억번의 연산을 돌려보았다. 연산은 임의로 지정했다.

class StreamTest {

    static long[] op1times = new long[10];
    static long[] op2times = new long[10];

    long start;
    long end;
    private final static int trial = 100000000;

    @BeforeEach
    void beforeEach() {
        start = System.currentTimeMillis();
    }

    @AfterAll
    static void afterAll() {
        System.out.println("test finished");
        double op1avg = Arrays.stream(op1times).average().getAsDouble();
        System.out.println("op1 took = " + op1avg + "ms in average");
        double op2avg = Arrays.stream(op2times).average().getAsDouble();
        System.out.println("op2 took = " + op2avg + "ms in average");
    }
    
    @RepeatedTest(10)
    void op1(RepetitionInfo repetitionInfo) {
        IntStream.range(1, trial)
                .filter(x -> x % 2 == 0)
                .filter(x -> x % 3 == 0)
                .filter(x -> x % 5 == 0)
                .filter(x -> x % 7 == 0)
                .filter(x -> x % 11 == 0)
                .boxed()
                .collect(Collectors.toList());

        end = System.currentTimeMillis();
        op1times[repetitionInfo.getCurrentRepetition() -1] = end - start;
    }

    @RepeatedTest(10)
    void op2(RepetitionInfo repetitionInfo) {
        IntStream.range(1, trial)
                .filter(x -> x % 2 == 0
                        && x % 3 == 0
                        && x % 5 == 0
                        && x % 7 == 0
                        && x % 11 == 0)
                .boxed()
                .collect(Collectors.toList());

        end = System.currentTimeMillis();
        op2times[repetitionInfo.getCurrentRepetition() -1] = end - start;
    }

}

@RepeatedTest로 10번을 반복했고, 한번의 테스트당 1억개의 원소를 가진 배열을 stream 연산 하였다.

각 테스트가 끝날 때 마다 걸린 시간을 기록했고, 모든 테스트가 끝난 후 걸린 시간의 평균을 출력해 보았다.

보다 정밀하게 테스트하기 위해서는 보다 자세한 지식이 필요하겠지만, 내가 원하는 결과를 얻기 위해서는 충분히 유의미한 결과를 얻어낼 수 있는 조건이 갖추어 졌다고 생각한다. (아니라면 댓글로 지적 부탁드립니다)

결과

test finished
op1 took = 634.2ms in average
op2 took = 160.8ms in average

가설로 세웠던 이론이 맞아 떨어졌다.

1억번의 연산 기준으로 평균 성능이 약 4배 차이나는 것으로 보아 Stream을 사용할 때 중간연산을 적게 사용하는 것이 성능에 도움이 될 수 있다는 것을 확인했다.

후기

  1. 성능테스트 하면서 어떻게 테스트 해야 결과를 더 정확하고 더 직관적으로 볼 수 있을 지 생각해 보는 경험을 하게 되었다.

  2. @AfterAll 과 @RepeatedTest 어노테이션, RepetitionInfo 라는 인터페이스를 사용해볼 수 있어 재미있었다.

profile
going up

0개의 댓글