중첩 for문 성능 차이
여러가지 루프의 성능차이에 대한 정리
✅ Nested For 루프
최근 회사에서 피크타임에 트래픽이 5배나 증가해 서버가 뻗어버리는 일이 있었다.
(이 일로 사업담당자분은 그날의 손해액을 계산하시느라 다크서클이 생기셨고, 우리팀도 야근을했다…😢)
원래도 성능 최적화에 관심이 있었는데,
실제로 장애를 겪고 보니 아예 목을 매는 수준까지 가는 것 같다.
아무튼 이번 주제인 중첩 for문에서의 성능차이에 대해 한번 실험을 해봤다.
@Benchmark
public void test1() {
long count = 0;
for(long i = 0; i < 10_000_000; i++) {
for(long j = 0; j < 100; j++) {
BigDecimal big = new BigDecimal("1000");
count++;
}
}
}
@Benchmark
public void test2() {
long count = 0;
for(long i = 0; i < 100; i++) {
for(long j = 0; j < 10_000_000; j++) {
BigDecimal big = new BigDecimal("1000");
count++;
}
}
}
test1() 은 외부 10,000,000회 / 내부 100회로 총 10억회의 루프를 돌며
test2() 은 외부 100회 / 내부 10,000,000회로 역시 총 10억회의 루프를 돈다.
루프 내부에 BigDecimal
을 생성하는 부분이 있는데,
그냥 루프에 약간의 부하를 주기 위한 장치다.
아무튼 도는 횟수는 같지만 유의미한 성능차이가 발생하였다.
결과부터 보자면
// test1()
Result "bench.ForLoop.test1":
15981.999 ±(99.9%) 1890.234 ms/op [Average]
(min, avg, max) = (15350.046, 15981.999, 16617.751), stdev = 490.888
CI (99.9%): [14091.766, 17872.233] (assumes normal distribution)
// test2()
Result "bench.ForLoop.test2":
16013.786 ±(99.9%) 520.377 ms/op [Average]
(min, avg, max) = (15836.663, 16013.786, 16182.794), stdev = 135.140
CI (99.9%): [15493.410, 16534.163] (assumes normal distribution)
JMH
로 JVM 워밍업
, 메모리 최적화
등을 가한 후 뽑아낸 결과다.
test1() 메소드의 경우 1.89초
의 수행 시간이 발생했고
test2() 메소드의 경우 0.52
초의 수행 시간이 발생했다.
그렇다면 이렇게 되는 원인이 대체 뭔가?라는 의문이 생긴다.
정확히 어떤원리로 이런 결과가 나오는 건지는 모르겠지만
내 짧은 지식으로는 한 가지 원인이 가장 유의미하다고 생각된다.
❓ 캐시와 오버헤드로 인한 성능차이
컴퓨터 구조와 운영체제 과목에서 데이터의 지역성에 관해 공간 지역성
, 시간 지역성
등의 내용이 있었다.
JVM에서 어느정도 다 최적화가 돼있긴 하겠지만, 아무리 생각해도 이 이유가 가장 크지않을까 싶다.
for(int i=0; i<10; i++;){}
for문도 내부적으로 지역변수 선언 / 조건 판단&분기 / 연산의 과정을 거치는데
이 일련의 과정들이 캐시 되고 이 최적화로인해 발생하는 오버헤드의 차이가
유의미한 성능의 차이로 나타나는게 아닐까?라는 생각이 강하다.
무슨 말이냐면,
for(long i = 0; i < 10_000_000; i++) {
for(long j = 0; j < 100; j++) {
BigDecimal big = new BigDecimal("1000");
count++;
}
}
이 경우 for 루프가 100회동안 선형 상태를 유지한다.
즉 for 루프의 방향성을 10,000,000회 갱신해야한다.
for(long i = 0; i < 100; i++) {
for(long j = 0; j < 10_000_000; j++) {
BigDecimal big = new BigDecimal("1000");
count++;
}
}
이 경우엔 for 루프가 10,000,000회 동안 선형 상태를 유지한다.
즉 for 루프의 방향성을 100회만 갱신하면 된다.
뭐 전부 다 추측일 뿐이고,
정확한 원리는 아직 모르겠지만
결론은 확실하다.
✅ 결론
더 작은 수를 for문의 외곽으로 뽑아낼수록 유의미한 성능 향상이 있다.
또 한 가지 유의사항으로는
// 좋지않음
for(int i=0; i<list.size(); i++){}
// 좋음
int size = list.size();
for(int i=0; j<size; i++){}
위 방법의 경우 주소 참조 연산(list.size())이 루프마다 일어나기 때문에
큰 배열을 루프할 때 좋지 않다.
이 경우에는 미리 주소를 참조하여 값을 스택에 할당해두고
계속 사용하는 두 번째 방법이 더 효율이 좋았다.