函数局部静态变量是不是会自动产生分支?

Posted

技术标签:

【中文标题】函数局部静态变量是不是会自动产生分支?【英文标题】:Does a function local static variable automatically incur a branch?函数局部静态变量是否会自动产生分支? 【发布时间】:2014-05-23 12:33:32 【问题描述】:

例如:

int foo()

    static int i = 0;
    return i++;

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

【问题讨论】:

When do function-level static variables get allocated/initialized?的可能重复 @cameron 这个问题是问什么时候,这个问题是问如何。 对于我的编译器在快速测试中,这 NOT 没有生成任何代码来在运行时初始化 i,它把变量放在一个内存位置,其中的值是从可执行文件加载的.尽管编译器必须像在第一次调用它时初始化值一样,但如果没有可见的副作用,它实际上不必这样做。如果它是没有构造函数的基本类型,它可能会避免整个开销。 @jcoder 如果我没记错的话,带有 constexpr 的现代 C++ 甚至可以在编译时初始化更复杂的对象。什么会有效地防止初始化代码中依赖于运行时的数据,可能实际上是任何无法声明为 constexpr 的东西。 【参考方案1】:

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

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

【讨论】:

有趣,所以这里保证 C 比 C++ 更高效,因为静态局部变量只能在 C 中使用编译时常量进行初始化。 他们可能只是使用文件范围变量。 C 的效率并不高。 @FredOverflow 一点也不;如果静态局部变量在 C 中有效,则允许 C++ 实现提前初始化它(实际上使用完全相同的机制,即.data 段)。 @FredOverflow 这就是你的 QOI;但是 C++ 实现很容易正确。您不能断定 C 肯定比 C++ 更高效;而是保证 C 的效率至少与 C++ 一样 这不是真的。 C根本不允许缓慢的情况。对于两种语言都允许的类似情况,它们同样快。对于其他情况,C++ 已经是赢家了,因为 C flat out 没有这个特性,无论性能如何。【参考方案2】:

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

示例

查看此代码:

#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 的回答(已被删除):代码可以自行修改。即,一旦初始化完成,只需删除初始化代码。然而,这个想法是不切实际的,原因如下:

    自修改代码很难实现线程安全。 通常,内存标记的可执行文件是写保护的,因此不允许代码自行重写。 只是不值得,因为分店不贵(见上文)。

【讨论】:

虽然展示一个装配示例是有益的,但它本身并不是一个明确的答案。 我反对将编译器特定的汇编结果作为证据,而不是参考实际的语言规则。 @Puppy 发布 gcc 所做的事情相当于说“通常”,这在实践中非常有帮助。 我过度使用静态变量来避免这个分支。现在我必须避免使用它们,因为它们甚至会导致未定义的行为,这取决于它们是如何被允许实现的。只是另一个毫无价值的 Cpp 工具,可以帮助你在脚下开枪。

以上是关于函数局部静态变量是不是会自动产生分支?的主要内容,如果未能解决你的问题,请参考以下文章

C中什么情况下把局部变量定义为局部静态变量

局部变量的存储类别--自动变量与静态局部变量

全局变量和局部变量

c/c++静态函数和静态成员函数

自动存储,静态存储和动态存储

C语言中,哪种存储类的作用域与生命周期是不一致的?