是的,有一个分支。每次输入函数时,代码必须检查变量是否已经初始化。但正如下面将要解释的,你通常不必关心这个分支。
示例
查看此代码:
#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 架构也根本不可能省略这里的分支。
分店贵吗?
考虑你是否应该关心这个分支:
你应该明确不关心这个分支,因为它会被正确预测(因为一旦对象被初始化,分支总是采用相同的路线)。因此,分支几乎是免费的。出于优化目的而尝试避免使用静态局部变量永远不会产生任何可观察到的性能优势。
真的没有办法绕过分支吗?
如果构造函数是不可观察的,就像简单地用常量值初始化,那么它可能会在程序启动时急切地执行并且分支被省略。但是,如果它是可观察到的,那么事情就会变得非常棘手:
我看到的唯一可能是 R. Martinho Fernandes 的回答(已被删除):代码可以自行修改。即,一旦初始化完成,只需删除初始化代码。然而,这个想法是不切实际的,原因如下:
- 自修改代码很难实现线程安全。
- 通常,内存标记的可执行文件是写保护的,因此不允许代码自行重写。
- 只是不值得,因为分店不贵(见上文)。