函数局部静态变量是否自动引发分支?

Does a function local static variable automatically incur a branch?

本文关键字:分支 静态 变量 是否 函数局      更新时间:2023-10-16

例如:

int foo()
{
    static int i = 0;
    return i++;
}

变量i只会在第一次调用foo时初始化为0。这是否意味着这里有一个隐藏的分支来防止初始化发生多次?还是有更聪明的方法来避免这种情况?

是的,它必须引起分支,并且必须引起至少一个原子操作,以实现安全的并发初始化。标准要求以一种并发安全的方式在函数入口初始化它们。

如果实现可以证明lazy init和一些更早的初始化(比如main())输入之前的初始化)之间的差异是相等的,那么它只能回避这个要求。例如,从常量初始化的简单pod,编译器可能会选择像文件作用域全局一样更早地初始化它,因为它是不可观察的,并且保存了延迟初始化代码,但这是一个不可观察的优化。

是的,有一个分支。每次输入函数时,代码必须检查变量是否已经初始化。但是正如下面将要解释的那样,你通常不必关心这个分支。

查看下面的代码:

#include <iostream>
struct Foo { Foo(){ std::cout << "FOO" << std::endl;} };
void foo(){ static Foo foo; }
int main(){ foo();}
现在,这是gcc4.8为foo函数生成的汇编代码的第一部分:
_Z3foov:
.LFB974:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA974
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
pushq   %r12
pushq   %rbx
.cfi_offset 12, -24
.cfi_offset 3, -32
movl    $_ZGVZ3foovE3foo, %eax
movzbl  (%rax), %eax
testb   %al, %al
jne .L7                     <------------------- FIRST CHECK
movl    $_ZGVZ3foovE3foo, %edi
call    __cxa_guard_acquire <------------------- LOCK    
testl   %eax, %eax
setne   %al
testb   %al, %al
je  .L7                     <------------------- SECOND CHECK
movl    $0, %r12d
movl    $_ZZ3foovE3foo, %edi

A你看,有一个jne !然后,使用__cxa_guard_acquireje获得一个守卫。因此,编译器似乎在这里生成了著名的双重检查锁定模式。

每个编译器都会生成一个分支吗?

我很确定规范没有强制要求必须使用分支或双重检查锁定。它只是要求初始化必须是线程安全的。但是,我没有看到没有分支就可以执行线程安全初始化的方法。因此,即使规范没有强制这样做,在当前的CPU体系结构中也不可能省略这里的分支。

分支贵吗?

考虑是否应该关心这个分支:你绝对不应该关心这个分支,因为它将被正确地预测(因为一旦对象初始化,分支总是采用相同的路由)。因此,该分支几乎是自由的。为了优化目的而避免使用静态局部变量永远不会产生任何可观察到的性能优势。

真的没有办法绕过这个分支吗?

如果构造函数不是可观察的,比如简单地用常量值初始化,那么它可以在程序启动时立即执行,而忽略分支。然而,如果它是可观察的,那么事情就变得相当棘手了:

我看到的唯一可能性是R. Martinho Fernandes的回答(已被删除):代码可以自我修改。也就是说,一旦初始化完成,只需删除初始化代码即可。然而,这个想法是不切实际的,原因如下:

    自修改代码很难获得线程安全。
  1. 通常,内存标记的可执行文件是写保护的,所以代码不允许重写自己。这是不值得的,因为分支并不昂贵(见上文)。