簡體   English   中英

編譯器如何實現 static 變量初始化?

[英]How is static variable initialization implemented by the compiler?

我很好奇 function 中 static 變量的底層實現。

如果我聲明一個基本類型(char、int、double 等)的 static 變量,並給它一個初始值,我想編譯器只是在main()稱為:

void SomeFunction();

int main(int argCount, char ** argList)
{
    // at this point, the memory reserved for 'answer'
    // already contains the value of 42
    SomeFunction();
}

void SomeFunction()
{
    static int answer = 42;
}

但是,如果 static 變量是 class 的實例:

class MyClass
{
    //...
};

void SomeFunction();

int main(int argCount, char ** argList)
{
    SomeFunction();
}

void SomeFunction()
{
    static MyClass myVar;
}

我知道在第一次調用 function 之前它不會被初始化。 由於編譯器無法知道第一次調用 function 的時間,它是如何產生這種行為的? 它本質上是否在 function 主體中引入了一個 if 塊?

static bool initialized = 0;
if (!initialized)
{
    // construct myVar
    initialized = 1;
}

在我看到的編譯器輸出中,函數局部靜態變量完全按照您的想象進行初始化。

請注意,通常這不是以線程安全的方式完成的。 因此,如果您具有可能從多個線程調用的靜態本地函數,則應考慮到這一點。 在調用任何其他函數之前在主線程中調用一次函數通常可以解決問題。

我應該補充一點,如果本地靜態的初始化是一個簡單的常量,就像在你的例子中一樣,編譯器不需要經歷這些旋轉 - 它可以只是初始化圖像中的變量,或者像main()一樣初始化main()靜態初始化(因為你的程序無法區分)。 但是如果用函數的返回值初始化它,那么編譯器幾乎必須測試一個標志,指示初始化是否已完成或等效。

這個問題涉及類似的問題 ,但未提及線程安全性。 對於它的價值,C ++ 0x將使函數靜態初始化線程安全。

(參見關於函數靜態的C ++ 0x FCD ,6.7 / 4:“如果控件在初始化變量時同時進入聲明,則並發執行應等待初始化完成。”)

另外一個沒有提到的是函數靜態以它們構造的相反順序被破壞,因此編譯器維護一個在關閉時調用的析構函數列表(這可能與atexit使用的列表相同或不同)。

你對一切都是正確的,包括初始化標志作為常見的實現。 這基本上是為什么靜態本地的初始化不是線程安全的,以及為什么存在pthread_once。

一個小小的警告:編譯器必須發出代碼,這些代碼“就像在第一次使用時構造靜態局部變量一樣”。 由於整數初始化沒有副作用(並且不調用用戶代碼),因此在初始化int時由編譯器決定。 用戶代碼不能“合法地”找出它的作用。

顯然,您可以查看匯編代碼,或激發未定義的行為並從實際發生的事情中扣除。 但是,C ++標准並不認為這是一個有效的理由,聲稱這種行為並非“好像”它按照規范所說的那樣行事。

我知道直到第一次調用該函數時才會初始化它。 由於編譯器無法知道第一次調用函數的時間,它是如何產生這種行為的? 它本質上是在函數體中引入if塊嗎?

是的,這是正確的:而且,FWIW,它不一定是線程安全的(如果函數是由兩個線程同時“第一次”調用的話)。

出於這個原因,您可能更喜歡在全局范圍內定義變量(盡管可能在類或命名空間中,或者在沒有外部鏈接的情況下靜態)而不是在函數內部,因此它在程序啟動之前初始化而沒有任何運行時“if” 。

另一個轉折是在嵌入式代碼中,其中run-before-main()代碼(cinit / whatever)可以將預初始化的數據(靜態和非靜態)從const數據段復制到ram中,可能駐留在ROM中。 在代碼可能無法從某種可以從中重新加載的后備存儲(磁盤)運行的情況下,這很有用。 同樣,這不違反語言的要求,因為這是在main()之前完成的。

稍微切線:雖然我沒有看到它做得太多(在Emacs之外),但程序或編譯器基本上可以在進程中運行代碼並實例化/初始化對象,然后凍結並轉儲進程。 Emacs做了類似的事情來加載大量的elisp(即咀嚼它),然后將運行狀態轉儲為工作可執行文件,以避免在每次調用時解析成本。

相關的東西不是 class 類型,而是初始化程序的編譯時評估(在當前優化級別)。 當然,構造函數沒有任何副作用,如果它不是微不足道的。

如果不能簡單地在.data中放置一個常量值,gcc/clang 使用一個保護變量的獲取負載來檢查 static 本地變量是否已初始化。 如果保護變量為假,則他們選擇一個線程進行初始化,如果他們也看到假保護變量,則讓其他線程等待它。 他們已經這樣做了很長時間,因為在 C++11 需要它之前。 (例如,從 2006 年 5 月開始,與 Godbolt 上的 GCC4.1 一樣古老。)

最簡單的人工示例,從第一次調用中截取 arg 並忽略后面的 args:

int foo(int a){
    static int x = a;
    return x;
}

使用 GCC11.3 -O3 ( Godbolt ) 為 x86-64 編譯,為-std=gnu++03模式生成完全相同的 asm。 GCC4.1 也產生了大致相同的 asm,但不會讓 push/pop 離開快速路徑(即缺少收縮包裝優化)。 GCC4.1 僅支持 AT&T 語法 output,因此它在視覺上看起來不同,除非您將現代 GCC 也切換到 AT&T 模式,但這是 Intel 語法(左側的目標)。

# demangled asm from g++ -O3
foo(int):
        movzx   eax, BYTE PTR guard variable for foo(int)::x[rip]  # guard.load(acquire)
        test    al, al
        je      .L13
        mov     eax, DWORD PTR foo(int)::x[rip]    # normal load of the static local
        ret              # fast path through the function is the already-initialized case


.L13:            # jumps here on guard == 0, on the first call (and any that race with it)
                 # It would be sensible for GCC to put this code in .text.cold
        push    rbx
        mov     ebx, edi             # save function arg in a call-preserved reg
        mov     edi, OFFSET FLAT:guard variable for foo(int)::x  # address
        call    __cxa_guard_acquire          # guard_acquire(&guard_x) presumably a normal mutex or spinlock
        test    eax, eax 
        jne     .L14                         # if (we won the race to do the init work) goto .L14
        mov     eax, DWORD PTR foo(int)::x[rip]  # else it's done now by another thread
        pop     rbx
        ret
.L14:
        mov     edi, OFFSET FLAT:guard variable for foo(int)::x
        mov     DWORD PTR foo(int)::x[rip], ebx       # init static x (from a saved in RBX)
        call    __cxa_guard_release
        mov     eax, DWORD PTR foo(int)::x[rip]       # missed optimization:  mov eax, ebx  
                # This thread is the one that just initialized it, our function arg is the value. 
                # It's not atomic (or volatile), so another thread can't have set it, too.
        pop     rbx
        ret

如果為 AArch64 編譯,保護變量的負載是ldarb w8, [x8] ,一個具有獲取語義的負載。 其他 ISA 可能需要一個簡單的加載,然后需要一個屏障來至少給出 LoadLoad 排序,以確保它們加載有效負載x的時間不早於他們看到保護變量非零時。


如果static變量具有常量初始化器,則不需要保護

int bar(int a){
    static int x = 1;
    return ++x + a;
}
bar(int):
        mov     eax, DWORD PTR bar(int)::x[rip]
        add     eax, 1
        mov     DWORD PTR bar(int)::x[rip], eax   # store the updated value
        add     eax, edi                          # and add it to the function arg
        ret

.section .data

bar(int)::x:
        .long   1

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM