在以下情况下应采取什么行为:
class C {
boost::mutex mutex_;
std::map<...> data_;
};
C& get() {
static C c;
return c;
}
int main() {
get(); // is compiler free to optimize out the call?
....
}
编译器是否允许优化对get()
的调用?
其思想是在多线程操作需要静态变量之前对其进行初始化
这是更好的选择吗?:
C& get() {
static C *c = new C();
return *c;
}
4条答案
按热度按时间yvgpqqbh1#
更新(2023)答案:
在C23(N4950)中,初始化静态局部变量的任何副作用都可以在进入其包含块时观察到。因此,除非编译器可以确定初始化变量没有可见的副作用,否则它将不得不生成代码以在适当的时候调用
get()
(或根据情况执行get()
的内联版本)。与早期的标准相反,C 23不再允许“早期”完成静态局部变量的动态初始化(如下所述)。
[stmt.dcl]/3:
具有静态存储持续时间(6.7.5.2)或线程存储持续时间(6.7.5.3)的块变量的动态初始化在控制第一次通过其声明时执行;这样的变量在其初始化完成时被认为是初始化的。
02 The Dog(2010)
C和C++标准在一个相当简单的原则下运行,通常被称为“as-if规则”-基本上,编译器可以自由地做几乎任何事情,只要没有符合标准的代码可以区分它所做的和官方要求的之间的差异。
我看不出有什么方法可以让符合规范的代码辨别在这种情况下是否真的调用了
get
,所以在我看来,优化它是免费的。至少在最近的N4296中,该标准包含了对静态局部变量进行早期初始化的显式权限:
具有静态存储持续时间的块作用域实体的常量初始化(3.6.2)(如果适用)在其块首次进入之前执行。在允许实现静态初始化命名空间作用域中具有静态或线程存储持续时间的变量的相同条件下,允许实现执行具有静态或线程存储持续时间的其他块作用域变量的早期初始化(3.6.2)。否则,在控制第一次通过其声明时初始化此类变量;这样的变量在其初始化完成时被认为是初始化的。
因此,在这个规则下,局部变量的初始化可以在执行的早期任意发生,所以即使它有可见的副作用,它们也允许在任何试图观察它们的代码之前发生。因此,您不能保证看到它们,因此允许对其进行优化。
y3bcpkx12#
根据您的编辑,这里有一个改进的版本,具有相同的结果。
输入:
输出量:
值得注意的是,没有为::get发出任何代码,但main仍然根据需要(在%2以及invasion.i和lpad.i的末尾)分配带有保护变量的::get::c(在%4)。llvm正在内联所有这些东西。
tl;dr:不用担心,优化器通常会正确处理这些东西。你看到一个错误?
wr98u20j3#
**您的原始代码是安全的。**不要引入额外的间接级别(在
std::map
的地址可用之前必须加载的指针变量)。正如Jerry Coffin所说,你的代码必须运行 * 就像 * 它是按照源代码顺序运行的一样。这包括在main中的后续内容(如启动线程)之前运行,就好像它已经构建了你的boost或
std::mutex
和std::map
。在C11之前,语言标准和内存模型并不是正式的线程感知的,但像这样的东西(线程安全的
static
-本地初始化)无论如何都能工作,因为编译器作者希望他们的编译器是有用的。例如,2006年的GCC 4.1(https://godbolt.org/z/P3sjo4Tjd)仍然使用guard变量with来确保在同时发生多个对get()
的调用时,单个线程进行构造。现在,在C11和更高版本中,ISO标准确实包括线程,并且官方要求它是安全的。
由于您的程序无法观察到这种差异,因此假设编译器可以选择跳过构造,让它在第一个线程中以未优化的方式实际调用
get()
。这很好,static
局部变量的构造是线程安全的,像GCC和Clang这样的编译器使用一个“保护变量”,它们在函数开始时检查(acquire
加载时为只读)。文件作用域的
static
变量将避免每次调用都会发生的guard变量的load+test/分支快速路径开销,并且只要在main()
开始之前没有任何东西调用get()
,它就是安全的。保护变量非常便宜,特别是在x86,AArch 64和32位ARMv 8等ISA上,这些ISA具有廉价的获取加载,但在ARMv7上更昂贵,例如获取加载使用dmb ish
全屏障。如果某个假设的编译器实际上做了您所担心的优化,那么差异可能是在页面的NUMA放置中包含
static C c
,如果该页面中没有其他内容被首先触及。如果在第二个线程也调用get()
时,构造还没有完成,那么可能会在其他线程第一次调用get()
时短暂地停止。当前的GCC和clang * don 't * 在实践中做了这种优化
Clang 17和libc为x86-64使用
-O3
创建了以下asm。(由Godbolt拆除)。get()
的asm也内联到main
中。GCC与libstdc非常相似,实际上只是在std::map
内部有所不同。即使
std::map
未使用,构造它也需要调用__cxa_atexit
(atexit
的C内部版本)来注册析构函数,以便在程序退出时释放红黑树。我怀疑这是优化器不透明的部分,也是它不能像static int x = 123;
或static void *foo = &bar;
那样优化到.data
中的预初始化空间而没有运行时构造(也没有保护变量)的主要原因。恒定传播避免了任何运行时初始化的需要 * 是 * 如果
struct C
只包括std::mutex
会发生的情况,在GNU/Linux中,std::mutex
至少没有析构函数,实际上是零初始化的。(C 23之前的C允许早期初始化,即使这包括 * 可见 * 的副作用。这个不一样编译器仍然可以在没有运行时调用的情况下将static int local_foo = an_inline_function(123);
恒定地传播到.data
中的一些字节中。GCC和Clang也不会优化guard变量(如果有任何运行时工作要做的话),即使
main
根本不会启动任何线程,更不用说在调用get()
之前了。其他编译单元(包括共享库)中的构造函数可能在main
调用的同时启动了另一个调用get()
的线程。(这可以说是gcc -fwhole-program
错过的优化。如果构造函数有任何(潜在的)* 可见 * 的副作用,可能包括对
new
的调用,因为new
是可替换的,编译器不能推迟它,因为 C 规则规定了构造函数何时在抽象机中被调用。(不过,允许用户对new
做一些假设,例如:clang with libc++可以优化掉new
/delete
以得到未使用的std::vector
。像
std::unordered_map
这样的类(一个哈希表而不是红黑树)在它们的构造函数中使用了new
。我用
std::map<int,int>
进行测试,所以单个对象没有具有可见副作用的析构函数。一个std::map<Foo,Bar>
,其中Foo::~Foo
打印了一些东西,这将使它在静态本地初始化器运行时变得重要,因为那是我们调用__cxa_atexit
的时候。假设析构顺序与构造顺序相反,那么等到以后再调用__cxa_atexit
可能会导致它被更快地析构,从而导致Foo::~Foo()
调用发生得太快,可能会在其他一些可见的副作用之前而不是之后。**或者其他全局数据结构可能会引用
std::map<int,int>
中的int
对象,并在其析构函数中使用这些对象。**如果我们过早地析构std::map
,那将是不安全的。(我不确定ISO C或GNU C是否为析构函数的排序提供了这样的顺序保证。但如果是这样的话,这就是编译器在涉及到注册析构函数时通常不能推迟构造的原因。在琐碎的程序中寻找这种优化是不值得花费编译时间的。)
使用file-scope
static
来避免保护变量请注意,缺少了一个保护变量,使得快速路径更快,特别是对于像ARMv7这样的ISA,它们没有一个好的方法来实现获取屏障。https://godbolt.org/z/4bGx3Tasj -
执行存储和调用
__cxa_atexit
的构造函数代码仍然存在,它只是在一个名为_GLOBAL__sub_I_example.cpp:
(clang)或_GLOBAL__sub_I_get():
(GCC)的单独函数中,编译器将其添加到main
之前调用的init函数列表中。函数作用域的本地变量通常很好,开销非常小,特别是在x86-64和ARMv 8上。但是由于您担心构建
std::map
时的微优化,因此我认为值得一提。并展示编译器用来使这些东西在引擎盖下工作的机制。3j86kqsm4#
编译器是否优化函数调用基本上是未指定的行为。一个未指定的行为基本上是从一组有限的可能性中选择的行为,但这种选择可能并不总是一致的。在这种情况下,选择是“优化”或“不”,标准没有规定,实现也不应该记录,因为它是一个给定实现可能不一致的选择。
如果这个想法只是“触摸”,如果我们只是添加一个虚拟的volatile变量并在每次调用中虚拟递增它,它会有帮助吗
例如