정적 지역 변수의 초기화 시점

JellyPower·2023년 4월 15일
0

나만 몰랐던 C++

목록 보기
1/11
post-thumbnail

코딩을 하면서 static변수를 쓰면서 최근 문득 든 생각이 있다. “static변수를 생성자에서 초기화하면 새로운 객체를 로드할 때마다 해당 값으로 덮어써지는거 아니야?” 라는 생각이었다.

물론 턱도 없는 생각이라는 걸 잘 알고 있고 그렇게 작동하지 않는다는 것쯤은 직관적으로 이해하고 있었지만 실제로 이가 어떻게 작동하는지가 궁금해 간단한 예제를 짜보았다.

  • 코드
#include <iostream>
using namespace std;

class MyClass{
    
public:
    MyClass(){
        static int a = 0; // 1번
        a++; // 2번
        cout<<a<<endl; // 3번
    }
};

int main()
{
    cout<<"Hello World"<<endl;;
    MyClass();
    MyClass();
    MyClass();
    MyClass();
    MyClass();

    return 0;
}
  • 결과

1
2
3
4
5

그 결과는 당연하게도 초기화는 한 번 만 일어난다는 것이었다. 그렇다면 드는 자연스러운 의문, 대체 초기화는 언제 하는건데? 그에 대한 해답은 아래 링크에 나와있다.
(아주 만족스럽게 설명이 잘 돼있다)

C++ 프로그래밍 :정적 지역 변수(Static Local Variable) 원리

.

.

.

.

.

.

라고 끝내기엔 너무 무책임하니 그 이유를 간단하게 요약해보도록 하겠다.

static 변수의 작동 원리

static변수라 함은 함수 호출시 스택에 쌓이는 형태로 동작하는 것이 아닌 전역 영역에 그 데이터가 저장되는 변수라는 것을 다들 알 것이다. 그렇기 때문에 전역 변수를 포함해서 이러한 정적 지역 변수들의 초기화, 생성 시점은 코드가 삽입돼있는 함수가 실행될 때가 아닌 프로세스의 시작 부분이라는 것이다.

때문에, 위처럼 생성자 내부에 초기화 코드를 박아놨어도 해당 변수는 프로세스의 시작에 초기화된다. 생성은 이미 돼있고 접근만 해당 스택 프레임에서 가능하다는 것이다.

실제로 컴파일을 하면 주석으로 적어놓은 2번, 3번 만 어셈블리의 함수 호출부에 임베딩되고 1번 에 해당하는 어셈블리 명령어는 찾아볼 수 없다.

  static int a = 0; // 대응하는 어셈블리 코드가 없다.
  a++;
00007FF7D2F722DF  mov         eax,dword ptr [a (07FF7D2F83440h)]  
00007FF7D2F722E5  inc         eax  
00007FF7D2F722E7  mov         dword ptr [a (07FF7D2F83440h)],eax

실제 위 코드는 Visual Studio의 디스어셈블리 창인데 static int a = 0; 에 해당하는 어셈블리 코드가 없는걸 볼 수 있다.

초기화가 상수로 일어나지 않는다면?

int ret2() {
	return 2;
}

void staticExam() {
	static int a = ret2();
	a++;
}

그러면 위와 같은 코드는 어떻게 동작할까? 상수의 초기화를 함수를 이용하고 있다.

이 말인 즉, 내가 MyFunc1()에 도달하기 전 모든 데이터가 이미 스택프레임에 올라가고 나서야 초기화가 가능하다는 것이고 위 스태틱 변수는 초기화 시점이 프로세스의 시작부분이 될 수 없다는 것이다.

이에 대한 대답은 해당 코드를 디어셈블리 해보면 알 수 있다.

	static int a = ret2();
00007FF77B641DAB  mov         eax,104h  
00007FF77B641DB0  mov         eax,eax  
00007FF77B641DB2  mov         ecx,dword ptr [_tls_index (07FF77B6524B0h)]  
00007FF77B641DB8  mov         rdx,qword ptr gs:[58h]  
00007FF77B641DC1  mov         rcx,qword ptr [rdx+rcx*8]  
00007FF77B641DC5  mov         eax,dword ptr [rax+rcx]  
00007FF77B641DC8  cmp         dword ptr [$TSS0 (07FF77B652450h)],eax  ; cmp 실행
00007FF77B641DCE  jle         staticExam+6Ch (07FF77B641DFCh)	; jle 실행
00007FF77B641DD0  lea         rcx,[$TSS0 (07FF77B652450h)]  
00007FF77B641DD7  call        _Init_thread_header (07FF77B641528h)  
00007FF77B641DDC  cmp         dword ptr [$TSS0 (07FF77B652450h)],0FFFFFFFFh  
00007FF77B641DE3  jne         staticExam+6Ch (07FF77B641DFCh)	; cmp, jne 실행
00007FF77B641DE5  call        ret2 (07FF77B641442h)  
00007FF77B641DEA  mov         dword ptr [a (07FF77B65244Ch)],eax  
00007FF77B641DF0  lea         rcx,[$TSS0 (07FF77B652450h)]  
00007FF77B641DF7  call        _Init_thread_footer (07FF77B641541h)  
	a++;
00007FF77B641DFC  mov         eax,dword ptr [a (07FF77B65244Ch)]	; 점프하고자하는 주소
00007FF77B641E02  inc         eax  
00007FF77B641E04  mov         dword ptr [a (07FF77B65244Ch)],eax

_Init_thread_header - threadSafeInit (_Init_thread_header가 어떻게 작동하는지 궁금하면 참조)

위 어셈블리 코드를 보도록하자. 사실 안봐도 상관 없다. (사실 나도 잘 모른다)

그저 코드에 cmp jle jne 명령어가 있다는 것만 보면 된다.

그게 뭐하는 명령어이냐? 바로 if문이라고 보면 된다.

즉 컴파일러는 자체적으로 해당 함수가 초기화됐는지를 if문으로 비교하여 처음에만 함수가 초기화 될 수 있도록 해준다고 보면 된다.

💡 static변수를 함수 등을 통해 런타임 초기화를 하면 C++이 자동적으로 `if문`을 생성해 주는 것을 알 수 있었다. 만약 해당 static 변수를 품는 함수가 빈번하게 호출되는 경우에는 파이프라이닝에 좋지 않은 결과를 만들어 낼 수 있으니 이를 조심해야겠다는 생각이 든다.
profile
게임엔진코드싸개(진)

0개의 댓글