어느날 코딩을 하다가 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을 사용할 때 중간연산을 적게 사용하는 것이 성능에 도움이 될 수 있다는 것을 확인했다.
성능테스트 하면서 어떻게 테스트 해야 결과를 더 정확하고 더 직관적으로 볼 수 있을 지 생각해 보는 경험을 하게 되었다.
@AfterAll 과 @RepeatedTest 어노테이션, RepetitionInfo 라는 인터페이스를 사용해볼 수 있어 재미있었다.