중첩 for문 성능 차이

중첩 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)

 

JMHJVM 워밍업, 메모리 최적화 등을 가한 후 뽑아낸 결과다.

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())이 루프마다 일어나기 때문에

큰 배열을 루프할 때 좋지 않다.

이 경우에는 미리 주소를 참조하여 값을 스택에 할당해두고

계속 사용하는 두 번째 방법이 더 효율이 좋았다.

 


© 2022. All rights reserved.