foo(x); foo(x); ... (10만번)과
for (int j = 0; j < 100000; j++) foo(x)와
뭐가 더 빠를까요?
다음과 같은 코드가 있습니다.
void foo(int& x) { x++; }
상기 코드를 10만번 반복을 해야 한다고 가정을 합시다. foo()를 10만번 쓰면 됩니다.
foo(x); foo(x); foo(x); foo(x); foo(x); ...
어느 세월에 씁니까? #define을 이용해서 코드를 반복시키겠습니다.
#define REP10(A) (A);(A);(A);(A);(A);(A);(A);(A);(A);(A);
#define REP100(A) REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);REP10(A);
#define REP1000(A) REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);REP100(A);
#define REP10000(A) REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);REP1000(A);
#define REP100000(A) REP10000(A);REP10000(A);REP10 ...
활용 예제입니다. LOOP_CNT(10000)만큼 돌리는 이유는 수행 10만개의 코드 수행 시간이 너무 짧기 때문에 실행을 반복하여 하도록 하여 시간을 재기 위함입니다.
void repeat() { int x = 0; for (int i = 0; i < LOOP_CNT; i++) { REP100000(foo(x)); // (A) } std::cout << x << std::endl; }
상기에서 (A)코드의 assembly를 보겠습니다. foo(x) function code(빨간 부분)가 lea, push, call, add esp 명령어의 반복이 10만번 되는 것을 확인할 수 있습니다(Assembly Code가 굉장히 깁니다).
; 21 : REP100000(foo(x)); lea ecx, DWORD PTR _x$[ebp] push ecx call ?foo@@YAXAAH@Z ; foo add esp, 4 lea edx, DWORD PTR _x$[ebp] push edx call ?foo@@YAXAAH@Z ; foo add esp, 4 lea eax, DWORD PTR _x$[ebp] push eax call ?foo@@YAXAAH@Z ; foo add esp, 4
비슷한 동작을 하는 코드(call이라는 function)을 작성합니다.
void call() { int x = 0; for (int i = 0; i < LOOP_CNT; i++) { for (int j = 0; j < 100000; j++) { foo(x); // (B) } } std::cout << x << std::endl; }
상기에서 (B) 코드의 assembly를 보겠습니다. foo function code(빨간 부분) 및 j를 이용한 반복 코드(파란 부분)가 들어 가 있는 것을 확인할 수 있습니다.
; 30 : { ; 31 : for (int j = 0; j < 100000; j++) mov DWORD PTR _j$18047[ebp], 0 jmp SHORT $LN3@call $LN2@call: mov ecx, DWORD PTR _j$18047[ebp] add ecx, 1 mov DWORD PTR _j$18047[ebp], ecx $LN3@call: cmp DWORD PTR _j$18047[ebp], 100000 ; 000186a0H jge SHORT $LN1@call ; 32 : { ; 33 : foo(x); lea edx, DWORD PTR _x$[ebp] push edx call ?foo@@YAXAAH@Z ; foo add esp, 4 ; 34 : } jmp SHORT $LN2@call $LN1@call: ; 35 : }
repeat function과 call function의 수행하기 이전의 tick과 이후의 tick을 계산하여 그 차이를 화면에 출력합니다.
begTick = GetTickCount();
repeat();
endTick = GetTickCount();
std::cout << endTick - begTick << std::endl;
begTick = GetTickCount();
call();
endTick = GetTickCount();
std::cout << endTick - begTick << std::endl;
실행 결과입니다.
[VS2005- Windows7](Windows7) 1000000000 9313 1000000000 4633 [gcc.exe (GCC) 4.5.2](Windows7) 1000000000 10218 1000000000 5335 [g++ (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5](Ubuntu 10.10) 1000000000 8483 1000000000 7293
코드의 수행 clock 만 따져 본다면 repeat() 는 카운트 증가 및 비교 코드가 없기 때문에 call()보다 빠를 것 같지만, 실제로 테스트를 해 보면 10만번 코드를 반복하는 것보다 for 문을 이용해서 반복시키는 것이 빠른 것을 확인할 수 있습니다. 하지만 이는 특수한 경우이며, 코드가 조금 달라 지거나 OS, Compiler 혹은 Memory Cache의 크기에 따라서 실행 결과는 달라 질 수 있습니다.
코드의 수행 속도를 빠르게 하기 위햐어 함수를 무조건 inline으로 해 주는 경우도 있는데(function call과 parameter들을 Stack으로 넘기는 load를 줄이기 위해서), 이 경우 컴파일되는 binary 코드의 길이가 커지고 Code Memory Cache Hit Miss를 발생시킬 수 있기 때문에 오히려 더 느려질 수도 있습니다. CPU의 발달로 인해 요즘에는 Assembly Code의 Clock 속도를 따지는 것은 점점 무의미해 져 가고 있습니다. function을 무조건 inline으로 하지 말고, Compiler의 최적화(Profile-Guided Optimization)에 맡기는 것이 좋습니다.
소스 코드 및 assembly 코드 첨부합니다. 컴파일은 VS 및 GCC(G++)에서 모두 컴파일이 가능합니다. Optimization을 주는 경우 컴파일러가 마음대로(?) 최적화를 하기 때문에 최적화 옵션을 빼고 테스트하였습니다( VS : Optimization - Disabled(/Od) / GCC : -O0 )