2014-09-24 39 views
10

對於線程安全惰性初始化,應該更喜歡函數內部的靜態變量,std :: call_once還是明確的雙重檢查鎖定?有什麼有意義的區別嗎?線程安全惰性初始化:靜態vs std :: call_once vs雙重檢查鎖定

這三個問題都可以在這個問題中看到。

Double-Checked Lock Singleton in C++11

兩個版本的雙重檢查C++ 11轉鎖定了在谷歌。

Anthony Williams shows雙重檢查鎖定與顯式內存順序和std :: call_once。他沒有提到靜態,但該文章可能是在C++ 11編譯器可用之前編寫的。

Jeff Preshing在廣泛的writeup中描述了雙重檢查鎖定的幾種變體。他確實提到了使用靜態變量作爲選項,他甚至表明編譯器將生成用於雙重檢查鎖定的代碼來初始化靜態變量。我不清楚他是否認爲一種方式比另一種更好。

我明白這兩篇文章的目的都是教學法,而且沒有理由這樣做。如果你使用靜態變量或std :: call_once,編譯器會爲你做。

+1

預先警告說VC++在線程安全函數local靜。他們不在VS2013中。但據報道是在VS2014:http://blogs.msdn.com/b/vcblog/archive/2014/06/11/c-11-14-feature-tables-for-visual-studio-14-ctp1。 aspx – 2014-09-24 15:07:08

+1

另一方面,GCC可以使本地靜態數據比call_once更快或者雙重檢查,因爲它可以使用平臺特定的技巧來避免任何原子操作。 – 2014-11-29 07:28:29

+1

@CortAmmon如果您將該帖子作爲回答並附帶一些證據,我會接受。 – Praxeolitic 2014-11-29 08:00:23

回答

15

GCC使用特定於平臺的技巧來完全避免原子操作在快速路徑上,利用它可以比call_once或雙重檢查更好地分析static的事實。

由於複覈使用原子作爲其避免種族情況的方法,因此每次都必須支付收購的價格。這不是一個高價,但它是一個價格。

它必須支付這個費用,因爲原子在所有情況下都必須保持原子,甚至像比較交換這樣的困難操作。這使得優化非常困難。一般來說,編譯器必須將其保留在中,以防您使用該變量不僅僅是雙鎖。它沒有簡單的方法來證明你從不使用原子上更復雜的操作之一。

另一方面,static是高度專業化和語言的一部分。它的設計從一開始就很容易初始化。因此,編譯器可以使用更通用版本不可用的快捷方式。 The compiler actually emits爲靜態下面的代碼:

一個簡單的函數:

void foo() { 
    static X x; 
} 

被改寫內部海灣合作委員會:

void foo() { 
    static X x; 
    static guard x_is_initialized; 
    if (__cxa_guard_acquire(x_is_initialized)) { 
     X::X(); 
     x_is_initialized = true; 
     __cxa_guard_release(x_is_initialized); 
    } 
} 

這看起來很像一個雙重檢查鎖。但是,編譯器會在這裏作弊。它知道用戶不能直接使用cxa_guard。它知道它只用於編譯器選擇使用它的特殊情況。因此,有了這些額外的信息,它可以節省一些時間。 CXA防護規範如同分佈式一樣共享一個common rule__cxa_guard_acquire決不會修改防護的第一個字節,並且__cxa_guard__release會將其設置爲非零。

這意味着每個守衛都必須是單調的,並且它明確指出哪些操作會這樣做。因此它可以利用主機平臺內現有的賽道保護。例如,在x86上,由強烈同步的CPU保證的LL/SS保護足以實現這種獲取/釋放模式,因此它可以在執行雙鎖時執行第一個字節的原始讀取,而不是獲取閱讀。這是唯一可能的,因爲GCC不使用C++原子API來執行雙重鎖定 - 它使用platform specific approach

在一般情況下,GCC無法優化原子。在被設計爲不太同步的體系結構(例如那些爲1024+核心設計的體系結構)上,GCC不會依賴架構來爲它做LL/SS。因此GCC被迫實際發射原子。但是,在x86和x64等常見平臺上,速度可能更快。

call_once可以具有GCC靜態效率,因爲它類似地限制了可以對once_flag執行的操作次數,以使其可以應用於原子的一小部分功能。權衡是靜態使用更爲方便,只要適用,但call_once適用於靜態不足的很多情況(例如由動態生成的對象擁有once_flag)。

在這些更高平臺上,靜態和call_once之間的性能稍有差異。許多這些平臺雖然不提供LL/SS,但至少可以提供非整數的非撕裂讀取。這些平臺可以使用這個和線程特定的指針來做per-thread epoch counting to avoid atomics。這對於靜態或call_once已足夠,但取決於計數器未翻轉。如果你沒有撕掉64位整數,call_once不得不擔心翻轉。實施可能會也可能不會擔心這一點。如果忽略這個問題,它可以像靜態一樣快。如果它注意到這個問題,它必須像原子一樣慢。 Static在編譯時知道有多少個靜態變量/塊,所以它可以證明在編譯時沒有翻轉(或者至少可以自信!)

+0

我意識到我對這個優秀的答案遲了好幾年。對於外行來說,「LL/SS保護」是什麼意思? – fbrereto 2017-01-13 17:51:55

+0

@fbrereto LL/SS是「加載加載/存儲存儲」它指出存儲地址的兩個加載保證按順序發生,並且保證存儲地址的兩個存儲按順序發生。然而,這種擔保並沒有說明貨物和商店相互之間的順序。 – 2017-01-13 18:39:19

+0

感謝您的澄清。另一個問題:我嘗試在https://godbolt.org/上面加載上面的'foo'示例,並且在GCC下面,我看到了在比較之前收到的調用。這是預期的嗎? https://godbolt.org/g/oV65PL – fbrereto 2017-01-13 19:09:52

相關問題