배경

3주차 기간 도중, 학우분이 슬랙방을 통해 C++의 static 지역 변수에 관한 기능을 얘기해주면서 시작됐습니다.

이 기능은 C와는 달랐습니다. C는 static 지역 변수는 상수로만 초기화할 수 있기 때문입니다. 그 이유는 생각해보면 간단합니다. static 변수의 초기화는 한번만 이루어지기 때문에, 컴파일 타임에 값이 결정되어야만 바이너리에 직접 삽입할 수 있기 때문입니다.

하지만 C++은 이러한 초기화가 런타임에 동적으로 수행될 수 있다고 했고, 이를 가능하게 하려면 C++ 컴파일러가 어떤 방식으로든 추가 변수나 코드를 삽입해 이를 구현했으리라 생각했습니다.

가설

제가 똑같은 기능을 만든다고 가정했을때, 빠르게 생각해본 초안은 아래 의사 코드와 같습니다.

// 사용자가 작성한 함수
int foo(int param)
{
	static int staticVariable = param;
	return staticVariable;
}

// 컴파일러가 추가한 함수
int foo(int param)
{
	static lock_t lock; // lock_t 구현 생략
	static bool isInitialized = false;
	static int staticVariable;
	
	lock.acquire();
	if (!isInitialized)
	{
		staticVariable = param;
		isInitialized = true;
	}
	lock.release();
	
	return staticVariable;
}

가설의 한계

위 구현은 기능은 만족하지만, 성능 측면에서 봤을때 한계점을 지니고 있습니다.

초기화는 한번만 이루어질텐데, 초기화 여부와 race condition 방지를 위해 매 호출마다 lock을 얻거나 대기해야하기 때문에 병렬성이 떨어지며 성능 면에서 좋지 않습니다.

실제 구현 분석

전체 코드

컴파일러가 삽입한 구현이면 바이너리 수준까지 내려가야 확인할 수 있습니다.

아래 어셈블리는 x86 Debug 빌드를 Visual Studio의 디스어셈블러를 통해 확인했습니다.

; static int staticVariable = param;
00321EE1  mov         eax,dword ptr [_tls_index (033042Ch)]
00321EE6  mov         ecx,dword ptr fs:[2Ch]
00321EED  mov         edx,dword ptr [ecx+eax*4]
00321EF0  mov         eax,dword ptr [$TSS0 (03303F0h)]
00321EF5  cmp         eax,dword ptr [edx+104h]
00321EFB  jle         __$EncStackInitStart+5Ch (0321F28h)
00321EFD  push        offset $TSS0 (03303F0h)
00321F02  call        __Init_thread_header (03215BEh)
00321F07  add         esp,4
00321F0A  cmp         dword ptr [$TSS0 (03303F0h)],0FFFFFFFFh
00321F11  jne         __$EncStackInitStart+5Ch (0321F28h)
00321F13  mov         eax,dword ptr [param]
00321F16  mov         dword ptr [staticVariable (03303ECh)],eax
00321F1B  push        offset $TSS0 (03303F0h)
00321F20  call        __Init_thread_footer (03215D7h)
00321F25  add         esp,4

분석

코드를 차례대로 나눠 분석해보면 아래와 같습니다.

00321EE1  mov         eax,dword ptr [_tls_index (033042Ch)]  ; TLS 배열 내 인덱스를 가져옴
00321EE6  mov         ecx,dword ptr fs:[2Ch]                 ; TLS 배열의 주소를 가져옴
00321EED  mov         edx,dword ptr [ecx+eax*4]              ; TLS 배열 내 해당 인덱스의 주소(32비트, 4바이트)를 가져옴
00321EF0  mov         eax,dword ptr [$TSS0 (03303F0h)]       ; TSS0 변수를 eax에 저장