为什么 clang 对某些全局变量执行线程安全 init,而对其他全局变量则不然?

Bee*_*ope 6 c++ global-variables clang static-initialization c++17

inline考虑使用C++ 17 中的新变量功能声明的全局(命名空间范围)变量:

\n
struct something {\n    something();\n    ~something();\n};\n\ninline something global;\n
Run Code Online (Sandbox Code Playgroud)\n

在 x86 上的 Clang 14 中,生成的用于在启动时初始化变量的程序集如下:

\n
__cxx_global_var_init:                  # @__cxx_global_var_init\n        push    rbx\n        mov     al, byte ptr [rip + guard variable for global]\n        test    al, al\n        je      .LBB0_1\n.LBB0_4:\n        pop     rbx\n        ret\n.LBB0_1:\n        mov     edi, offset guard variable for global\n        call    __cxa_guard_acquire\n        test    eax, eax\n        je      .LBB0_4\n        mov     edi, offset global\n        call    something::something() [complete object constructor]\n        mov     edi, offset something::~something() [complete object destructor]\n        mov     esi, offset global\n        mov     edx, offset __dso_handle\n        call    __cxa_atexit\n        mov     edi, offset guard variable for global\n        pop     rbx\n        jmp     __cxa_guard_release             # TAILCALL\n        mov     rbx, rax\n        mov     edi, offset guard variable for global\n        call    __cxa_guard_abort\n        mov     rdi, rbx\n        call    _Unwind_Resume@PLT\nglobal:\n        .zero   1\n\nguard variable for global:\n        .quad   0                               # 0x0\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个双重检查锁定模式,它会导致线程安全的初始化过程:第一个进行test al, al初始乐观检查以查看变量是否已初始化,如果表明变量尚未初始化,则执行 \xe2\x80\x93 。未初始化 \xe2\x80\x93 进行调用,该调用__cxa_guard_acquire将再次检查锁下的同一变量,以避免两个或多个线程都“通过”初始检查的竞争:只有一个线程会“通过”第二次检查。

\n

此模式与用于初始化非平凡类型的函数局部静态变量的模式相同(标准要求延迟初始化这些变量)。

\n

我们还可以查看“模板静态持有者”模式的程序集,该模式通常用于在 C++17 之前的标头中实现全局变量,如下所示:

\n
struct something {\n    something();\n    ~something();\n};\n\ntemplate <typename T = void>\nstruct holder {\n    static something global;\n};\n\ntemplate <typename T>\nsomething holder<T>::global;\n\n\nvoid instantiate() {\n    (void)holder<void>::global;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,该类holder允许holder<T>::global在多个翻译单元中实例化,并要求这项工作(“让链接器对其进行排序”),这与非模板类中的命名空间范围全局变量或静态变量的情况不同。该instantiate()调用只是为了实际实例化模板和关联的静态成员,否则根本不会生成任何内容。

\n

组装如下

\n
instantiate():                       # @instantiate()\n        ret\n__cxx_global_var_init:                  # @__cxx_global_var_init\n        push    rax\n        cmp     byte ptr [rip + guard variable for holder<void>::global], 0\n        je      .LBB1_1\n        pop     rax\n        ret\n.LBB1_1:\n        mov     edi, offset holder<void>::global\n        call    something::something() [complete object constructor]\n        mov     edi, offset something::~something() [complete object destructor]\n        mov     esi, offset holder<void>::global\n        mov     edx, offset __dso_handle\n        call    __cxa_atexit\n        mov     byte ptr [rip + guard variable for holder<void>::global], 1\n        pop     rax\n        ret\nholder<void>::global:\n        .zero   1\n\nguard variable for holder<void>::global:\n        .quad   0                               # 0x0\n
Run Code Online (Sandbox Code Playgroud)\n

双重检查锁定消失了:守卫变量仅在任何锁之外检查一次。

\n

为什么有区别?这只是一个实施怪癖,还是以某种方式源于标准中的要求?

\n

看起来这些全局构造函数通常不需要锁,因为这些生成的函数通常在启动时在main到达之前或在动态加载共享对象时在单线程代码中调用。然而,也许有一些我没有考虑的场景,例如并行加载两个引用相同全局的共享对象?

\n