例如:
int foo()
{
static int i = 0;
return i++;
}
Run Code Online (Sandbox Code Playgroud)
该变量i仅0在第一次被初始化时foo被调用.这是否自动意味着在那里有一个隐藏的分支,以防止初始化不止一次发生?还是有更聪明的技巧来避免这种情况?
Pup*_*ppy 18
是的,它必须产生一个分支,并且它还必须至少产生一个原子操作以进行安全的并发初始化.标准要求它们以函数入口初始化,并行安全.
如果能够证明延迟初始化和某些早期初始化之间的差异(如输入main()之前)之间的差异是等效的,那么实现只能避免此要求.例如,从常量初始化的简单POD,编译器可能选择像文件范围全局一样早期初始化它,因为它是不可观察的并且保存了延迟初始化代码,但这是一个不可观察的优化.
gex*_*ide 14
是的,有一个分支.每次输入函数时,代码都必须检查变量是否已经初始化.但正如下面将要解释的那样,您通常不必关心这个分支.
看看这段代码:
#include <iostream>
struct Foo { Foo(){ std::cout << "FOO" << std::endl;} };
void foo(){ static Foo foo; }
int main(){ foo();}
Run Code Online (Sandbox Code Playgroud)
现在,这是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
Run Code Online (Sandbox Code Playgroud)
你看,有一个jne!然后,获得一名后卫使用__cxa_guard_acquire,然后是a je.因此,似乎编译器在这里生成着名的双重检查锁定模式.
我很确定规范并未强制要求必须使用分支或双重检查锁定.它只是要求初始化必须是线程安全的.但是,我没有看到在没有分支的情况下执行线程安全初始化的方法.因此,即使规范没有强制要求,当前的CPU体系结构根本不可能省略这里的分支.
考虑你是否应该关心这个分支:你应该不关心这个分支,因为它将被正确预测(因为一旦对象被初始化,分支总是采用相同的路径).因此,分支几乎是免费的.尝试避免用于优化目的的静态局部变量永远不会产生任何可观察到的性能优势.
如果构造函数不可观察,就像简单地使用常量值初始化那样,那么可以在程序启动时急切地执行它,并省略分支.但是,如果它是可观察的,那么事情变得非常棘手:
我看到的唯一可能性是R. Martinho Fernandes的答案(已被删除):代码可以修改自己.即,只需在初始化完成后删除初始化代码.但是,由于以下原因,这个想法是不切实际的:
| 归档时间: |
|
| 查看次数: |
1481 次 |
| 最近记录: |