gcc对C++局部静态变量初始化相关
一、靜態局部變量初始化是否會很耗
之前曾經注意到過,gcc對靜態變量的運行時初始化是考慮到多線程安全的,也就是說對于工程中大量使用的單間對象: CSingletone::Instance類型的代碼,理論上說都是要經過mutex這種重量級的互斥檢測,如此看來,這種單間對象對系統損耗應該是非常大的,因為隨手寫的每個instance的調用都可能會由編譯器插入一個互斥鎖的獲得和釋放動作。下面是一個簡單的測試代碼,其中靜態變量FOO的初始化執行流層是對靜態局部變量初始化判斷邏輯的原型提煉。可以看到,這個簡單的代碼讓編譯器生成了很多指令,其中包括了保證變量唯一一次性初始化的不可見變量,以及其它的一些異常棧幀的處理,關于異常處理的相關代碼就不再深入了,這里只關注對于靜態變量初始化的考慮。它的關鍵問題是保證靜態變量是在第一次被調用的時候被初始化并且只初始化一次,考慮到多線程的環境下,這個代碼還要多線程安全,所以編譯器在其中插入了互斥鎖的操作。
tsecer@harry: cat statinit.cpp
struct FOO
{
FOO() { void somehow(); somehow();}
};
int bar()
{
static FOO foo;
}
tsecer@harry: g++ -S statinit.cpp
tsecer@harry: cat statinit.s |c++filt
.file "statinit.cpp"
.section .text._ZN3FOOC1Ev,"axG",@progbits,FOO::FOO(),comdat
.align 2
.weak FOO::FOO()
.type FOO::FOO(), @function
FOO::FOO():
.LFB4:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
subq $16, %rsp
.LCFI2:
movq %rdi, -8(%rbp)
call somehow()
leave
ret
.LFE4:
.size FOO::FOO(), .-FOO::FOO()
.globl __gxx_personality_v0
.globl _Unwind_Resume
.text
.align 2
.globl bar()
.type bar(), @function
bar():
.LFB5:
pushq %rbp
.LCFI3:
movq %rsp, %rbp
.LCFI4:
subq $32, %rsp
.LCFI5:
movl guard variable for bar()::foo, %eax
movzbl (%rax), %eax
testb %al, %al
jne .L11
movl guard variable for bar()::foo, %edi
call __cxa_guard_acquire
testl %eax, %eax
setne %al
testb %al, %al
je .L11
movb $0, -9(%rbp)
movl bar()::foo, %edi
.LEHB0:
call FOO::FOO()
.LEHE0:
movl guard variable for bar()::foo, %edi
call __cxa_guard_release
jmp .L11
.L12:
movq %rax, -24(%rbp)
.L7:
movq -24(%rbp), %rax
movq %rax, -8(%rbp)
movzbl -9(%rbp), %eax
xorl $1, %eax
testb %al, %al
je .L8
movl guard variable for bar()::foo, %edi
call __cxa_guard_abort
.L8:
movq -8(%rbp), %rax
movq %rax, -24(%rbp)
movq -24(%rbp), %rdi
.LEHB1:
call _Unwind_Resume
.LEHE1:
.L11:
leave
ret
.LFE5:
.size bar(), .-bar()
.section .gcc_except_table,"a",@progbits
.LLSDA5:
.byte 0xff
.byte 0xff
.byte 0x1
.uleb128 .LLSDACSE5-.LLSDACSB5
.LLSDACSB5:
.uleb128 .LEHB0-.LFB5
.uleb128 .LEHE0-.LEHB0
.uleb128 .L12-.LFB5
.uleb128 0x0
.uleb128 .LEHB1-.LFB5
.uleb128 .LEHE1-.LEHB1
.uleb128 0x0
.uleb128 0x0
.LLSDACSE5:
.text
.local guard variable for bar()::foo
.comm guard variable for bar()::foo,8,8
.local bar()::foo
.comm bar()::foo,1,1
.section .eh_frame,"a",@progbits
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
二、gcc內運行時庫代碼支持
可以看到,在__cxa_guard_acquire函數內部使用了一個互斥鎖,這個通常是一個代碼很高的消耗(雖然當前mutex使用futex類型的用戶態鎖操作,但是考慮到可能有掛起操作,這個代碼也不能忽略)。這個互斥鎖有兩個特點,一個是它可以遞歸(遞歸初始化一個變量通常意味著錯誤,將會拋出recursive_init類型異常),另一個是進程內所有線程共享同一個鎖(這意味著在多線程環境下,如果一個線程下的靜態變量構造函數中有死循環,其它所有線程如果執行到構造函數將會被掛起,不過這個現象我沒有測試)。
gcc-4.1.0libstdc++-v3libsupc++guard.cc
namespace __cxxabiv1
{
static inline int
recursion_push (__guard* g)
{
return ((char *)g)[1]++;
}
static inline void
recursion_pop (__guard* g)
{
--((char *)g)[1];
}
static int
acquire_1 (__guard *g)
{
if (_GLIBCXX_GUARD_TEST (g))
return 0;
if (recursion_push (g))
{
#ifdef __EXCEPTIONS
throw __gnu_cxx::recursive_init();
#else
// Use __builtin_trap so we don't require abort().
__builtin_trap ();
#endif
}
return 1;
}
extern "C"
int __cxa_guard_acquire (__guard *g)
{
#ifdef __GTHREADS
// If the target can reorder loads, we need to insert a read memory
// barrier so that accesses to the guarded variable happen after the
// guard test.
if (_GLIBCXX_GUARD_TEST_AND_ACQUIRE (g))
return 0;
if (__gthread_active_p ())
{
// Simple wrapper for exception safety.
struct mutex_wrapper
{
bool unlock;
mutex_wrapper (): unlock(true)
{
static_mutex::lock ();
}
~mutex_wrapper ()
{
if (unlock)
static_mutex::unlock ();
}
} mw;
if (acquire_1 (g))//這里的acquire_1表示的是獲取一個int類型g的第1個char的值,是否初始化使用的是第0個char值。
{
mw.unlock = false;
return 1;
}
return 0;
}
#endif
return acquire_1 (g);
}
extern "C"
void __cxa_guard_abort (__guard *g)
{
recursion_pop (g);
#ifdef __GTHREADS
if (__gthread_active_p ())
static_mutex::unlock ();
#endif
}
extern "C"
void __cxa_guard_release (__guard *g)
{
recursion_pop (g);
_GLIBCXX_GUARD_SET_AND_RELEASE (g);
#ifdef __GTHREADS
if (__gthread_active_p ())
static_mutex::unlock ();
#endif
}
}
三、用戶代碼和gcc庫代碼的結合
這里的實現思路就是首先判斷該靜態變量是否被初始化的標志位是否已經置位,如果置位說明該變量的構造函數已經被執行完成,此時直接跳過構造函數;如果該值非零,就要嘗試來進行該變量構造函數的執行,也就是調用__cxa_guard_acquire函數,該函數返回值有兩個,1表示說獲得了該變量的初始化權,如果為0表示說本以為會獲得,但是經過互斥判斷之后并沒有,總之就是說不用來執行靜態變量的初始化。
進而在__cxa_guard_acquire函數內部,它再次判斷該變量是否被初始化的標志位,如果沒有則嘗試獲得互斥鎖,獲得互斥鎖之后,需要再次判斷是否初始化,如果已經被人初始化,則返回0,否則返回1,此時該函數的調用者就有責任執行該變量的初始化并進而執行__cxa_guard_release,該函數負責設置變量已經完全初始化成功的標志位并釋放全局互斥鎖。
上面的文字肯定讓人非常費解,最好的辦法是畫一幅圖來對比各個流程,但是我不會畫,好在有一個意義及代碼和該處邏輯非常相似的函數就是ptrehad庫中的pthread_once,C庫中它代碼為glibc-2.6
ptlpthread_once.c:
int
__pthread_once (once_control, init_routine)
pthread_once_t *once_control;
void (*init_routine) (void);
{
/* XXX Depending on whether the LOCK_IN_ONCE_T is defined use a
global lock variable or one which is part of the pthread_once_t
object. */
if (*once_control == PTHREAD_ONCE_INIT)
{
lll_lock (once_lock);
/* XXX This implementation is not complete. It doesn't take
cancelation and fork into account. */
if (*once_control == PTHREAD_ONCE_INIT)
{
init_routine ();
*once_control = !PTHREAD_ONCE_INIT;
}
lll_unlock (once_lock);
}
return 0;
}
strong_alias (__pthread_once, pthread_once)
四、__cxa_guard_acquire返回0的一種情況
線程A 線程B
時 A 獲得mutex_wrapper::static_mutex
間 B 阻塞在mutex_wrapper中static_mutex
| A 初始化局部變量
|
| A 執行 __cxa_guard_release釋放mutex_wrapper::static_mutex
/
B 被喚醒 acquire_1函數滿足if (_GLIBCXX_GUARD_TEST (g)) return 0;
五、拋出recursive_init異常
tsecer@harry: cat staticrec.cpp
struct FOO
{
FOO() { static FOO foo; }
};
int main()
{
static FOO foo;
}
tsecer@harry: g++ staticrec.cpp
tsecer@harry: ./a.out
terminate called after throwing an instance of '__gnu_cxx::recursive_init'
what(): N9__gnu_cxx14recursive_initE
已放棄 (core dumped)
tsecer@harry:
六、結論
靜態變量的初始化即保證了多線程安全,又保證了效率,絕大部分情況下,只是幾條內存值判斷,在下面的幾條語句執行之后跳過初始化代碼的執行
movl guard variable for bar()::foo, %eax
movzbl (%rax), %eax
testb %al, %al
jne .L11
總結
以上是生活随笔為你收集整理的gcc对C++局部静态变量初始化相关的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL select查询原理--查询语句
- 下一篇: Label 表达式绑定