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