[C/C++] 변수 초기화의 오버헤드
다음의 두 코드 조각,
char * arr[100];
와
char * arr[100] = { (char *) 0, };
는 심각한 성능 차이를 보인다. 자주 호출되는 함수나 loop 안에 이런 코드를 넣으면, 성능 저하가 크게 나타날 것이다.
비록 코드 상으로는 배열의 첫번째 element만 초기화하는 것으로 보이지만, 실제로는 배열의 모든 element에 대해서 초기화하는 작업이 이루어진다. assembly code 수준에서는 memset으로 배열이 차지하는 메모리 공간을 모두 0으로 set하는 것과 동일하다.
컴파일러가 최적화(optimization)를 하지 않은 상태에서는(-00), 다음과 같은 assembly code가 생성된다. (GCC 3.4.6, x86_64)
leaq -800(%rbp), %rdi movl $800, %edx movl $0, %esi call memset
이 코드는 memset을 호출하기 전에 첫번째 argument로 이 arr 배열의 주소를 64비트로 해서 저장하고, 두번째 argument로 0의 초기값을, 세번째 argument로 800바이트(64비트 * 100개)의 길이를 지정한 다음, memset을 호출하는 것이다. C 코드로는 다음과 같다.
memset(arr, 0, 800);
최적화 수준을 높이더라도 약간의 assembly code 상으로 차이가 있긴 하지만, 결과적으로는 배열의 메모리 공간 전체에 대한 초기화가 수행된다.
참고로 최적화 수준별로 assembly code를 요약해본다. 코드 순서가 조금씩 뒤바뀌어 있고, opcode가 조금씩 다르다.
-O1
movq %rsp, %rdi movl $800, %edx movl $0, %esi call memset
-O2, -O3
movl $800, %edx xorl %esi, %esi movq %rsp, %rdi call memset
-O2, -O3의 색다른 점은, 0값을 register에 저장하기 위해 0값을 직접 저장하는 게 아니라 해당 register를 xor로 현재 set된 값을 reset하는 방식을 사용한다는 점이다.
댓글 4개
산오리
gcc 4.3 이상에서도 같은 코드가 생성되나요?
uriel
x86 gcc 4.2 (ubuntu hardy)에서 -O에 따라 다르게 컴파일 되더군요.
1) -O0
8048388: 8d 95 6c fe ff ff lea -0x194(%ebp),%edx
804838e: b8 90 01 00 00 mov $0x190,%eax
8048393: 89 44 24 08 mov %eax,0x8(%esp)
8048397: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
804839e: 00
804839f: 89 14 24 mov %edx,(%esp)
80483a2: e8 21 ff ff ff call 80482c8
2) -O1 이상
해당 초기화 루틴은 아무런 일을 하지 않습니다. 아마도 -O1 이상이면 초기화를 하지 않게 되어 있나보더군요.
terzeron
제가 테스트한 버전은 GCC 3.4.6입니다. C냐 C++이냐는 별 차이 없었던 듯 합니다
.
산오리님께서 지적하신 대로 4.3.2에서 테스트해보니 일단 -O1 이상에서는 아예 초기화 코드가 없어졌습니다. 그리고 -O0에서도 특별히 memset처럼 배열 전체를 초기화하는 것 같지 않습니다. 32비트 머신에서만 테스트를 해봤는데, 확인을 위해 64비트 머신에서도 테스트해보려고 합니다.
terzeron
GCC 4.3.2를 CentOS 64비트 머신 두 대에 설치해보려 했으나 실패했습니다. 필요한 라이브러리까지 설치해서 32비트 머신에서는 설치가 되었는데 이번에는 안 되네요. 아직까지는 64비트 머신에 대해 제대로 테스트가 되지 않은 듯 합니다.