一, 静态局部变量初始化是否会很耗
之前曾经注意到过, 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.0\libstdc++-v3\libsupc++\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\nptl\pthread_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
来源: http://www.bubuko.com/infodetail-2979029.html