函数本地静态变量是否会自动产生分支?

时间:2014-05-23 12:33:33

标签: c++

例如:

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

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

2 个答案:

答案 0 :(得分:15)

是的,它必须产生一个分支,并且它还必须至少一个原子操作才能进行安全的并发初始化。标准要求它们以函数入口初始化,并行安全。

如果能够证明延迟初始化和某些早期初始化之间的区别(如输入main()之前)之间的差异是等效的,则实现只能避免此要求。例如,从常量初始化的简单POD,编译器可能会选择像文件范围全局一样更早地初始化它,因为它是不可观察的并且保存了延迟初始化代码,但这是一个不可观察的优化。

答案 1 :(得分:10)

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

实施例

查看此代码:

#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
你看,有一个jne!然后,使用__cxa_guard_acquire获取后卫,然后使用je。因此,似乎编译器在这里生成了着名的double checked locking pattern

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

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

分支是否昂贵?

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

分支机构真的没办法吗?

如果构造函数不可观察,就像简单地使用常量值初始化一样,那么可以在程序启动时急切地执行它,并省略分支。但是,如果它是可观察的,那么事情变得非常棘手:

我看到的唯一可能性是在R. Martinho Fernandes(已被删除)的答案中说明:代码可以自行修改。即,只需在初始化完成后删除初始化代码即可。但是,由于以下原因,这个想法是不切实际的:

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