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.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