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 )


rep_test.zip