簡體   English   中英

訪問靜態函數變量比訪問全局變量要慢嗎?

[英]Is access to a static function variable slower than access to a global variable?

靜態局部變量在第一個函數調用時初始化:

在塊作用域中使用指定符static聲明的變量具有靜態存儲持續時間,但是在控件第一次通過其聲明時初始化(除非它們的初始化為零或初始化初始化,這可以在首次輸入塊之前執行)。 在所有進一步的調用中,將跳過聲明。

此外,在C ++ 11中還有更多檢查:

如果多個線程同時嘗試初始化相同的靜態局部變量,則初始化只發生一次(使用std :: call_once可以獲得任意函數的類似行為)。 注意:此功能的常規實現使用雙重檢查鎖定模式的變體,這可以將已初始化的局部靜態的運行時開銷減少到單個非原子布爾比較。 (自C ++ 11以來)

同時, 全局變量似乎在程序啟動時初始化 (盡管技術上只在cppreference上提到了分配 / 解除分配 ):

靜態存儲時間。 程序開始時分配對象的存儲空間,程序結束時分配存儲空間。 只存在一個對象實例。 在命名空間范圍(包括全局命名空間)聲明的所有對象都具有此存儲持續時間,以及使用static或extern聲明的持續時間

所以給出以下示例:

struct A {
    // complex type...
};
const A& f()
{
    static A local{};
    return local;
}

A global{};
const A& g()
{
    return global;
}

我是否正確假設f()必須檢查每次調用它的變量是否被初始化,因此f()將比g()慢?

當然,你在概念上是正確的,但現代建築可以解決這個問題。

現代編譯器和體系結構將安排管道,以便假定已經初始化的分支。 因此,初始化的開銷會產生額外的管道轉儲,這就是全部。

如果您有任何疑問,請檢查組件。

是的,它幾乎肯定會稍微慢一些。 然而,大部分時間它都無關緊要,成本將超過“邏輯和風格”的好處。

從技術上講,函數本地靜態變量與全局變量相同。 只是它的名稱不是全局已知的(這是一件好事),並且它的初始化保證不僅發生在確切的指定時間,而且只發生一次,並且線程安全。

這意味着函數本地靜態變量必須知道初始化是否已經發生,因此需要至少一個額外的內存訪問和一個全局(原則上)不需要的條件跳轉。 實現可能會對全局變量做類似的事情,但它不需要 (通常也不需要 )。

在所有情況下都可以正確預測跳躍,但有兩個跳躍是很好的。 前兩個調用很可能被預測為錯誤(通常默認假設是跳過,而不是第一次調用時的錯誤假設,並且假設后續跳轉采用與最后一個相同的路徑,同樣錯誤)。 在那之后,你應該好好去,接近100%正確的預測。
但即使是正確預測的跳轉也不是免費的(CPU仍然只能在每個周期啟動給定數量的指令,即使假設它們沒有時間完成),但它並不多。 如果可以成功隱藏在最壞情況下可能是幾百個周期的存儲器延遲,則流水線中的成本幾乎消失。 此外,每次訪問都會獲取一個額外的高速緩存行,否則不需要該高速緩存行(已經初始化的標志可能不會存儲在與數據相同的高速緩存行中)。 因此,你的L1性能稍差(L2應該足夠大,所以你可以說“是的,那么什么”)。

它還需要實際執行一次並且線程安全全局(原則上)不必執行,至少不是以您看到的方式執行。 實現可以做一些不同的事情,但大多數只是在輸入main之前初始化全局變量,並且很少大部分是使用memset完成的,或者是隱式的,因為變量存儲在無論如何都歸零的段中。
執行初始化代碼時, 必須初始化靜態變量,並且必須以線程安全的方式進行。 根據您的實施情況糟透了,這可能非常昂貴。 我決定放棄線程安全功能,並且在發現GCC(否則是一個OK allround編譯器)實際上會鎖定每個靜態的互斥鎖后,總是用fno-threadsafe-statics編譯(即使這不符合標准)初始化。

來自https://en.cppreference.com/w/cpp/language/initialization

延遲動態初始化
它是實現定義的,是否在主函數的第一個語句(用於靜態)或線程的初始函數(用於線程本地)之前發生動態初始化,或者延遲發生在之后。

如果非內聯變量(因為C ++ 17)的初始化延遲發生在主/線程函數的第一個語句之后,它發生在任何變量的第一次使用之前,其中靜態/線程存儲持續時間定義在與要初始化的變量相同的翻譯單元。

因此,對於全局變量也可能需要進行類似的檢查。

所以f()不必比g() “慢”

g()不是線程安全的,並且容易受到各種排序問題的影響。 安全將付出代價。 有幾種支付方式:

f() ,Meyer的Singleton,為每次訪問付出代價。 如果頻繁訪問或在代碼的性能敏感部分訪問,那么避免使用f()確實有意義。 您的處理器可能具有可用於分支預測的有限數量的電路,並且您無論如何都被迫在分支之前讀取原子變量。 僅僅確保初始化只發生一次,這是一個很高的代價。

h() ,如下所述,與g()非常相似,具有額外的間接性,但假設h_init()在執行開始時只被調用一次。 最好你定義一個被調用為main()行的子程序; 它使用絕對排序調用像h_init()這樣的每個函數。 希望這些物體不需要被破壞。

或者,如果使用GCC,則可以使用__attribute__((constructor))注釋h_init() __attribute__((constructor)) 我更喜歡靜態init子例程的顯式性。

A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }

h2()就像h() ,減去額外的間接:

alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }

暫無
暫無

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

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