对于线程安全延迟初始化,是否应该更喜欢函数内部的静态变量,std :: call_once或显式双重检查锁定?有什么有意义的差异吗?
这个问题都可以看出这三个问题。
Double-Checked Lock Singleton in C++11
C ++ 11中的两个双重检查锁定版本出现在Google中。
Anthony Williams shows使用显式内存排序和std :: call_once双重检查锁定。他没有提到静态,但该文章可能是在C ++ 11编译器可用之前编写的。
Jeff Preshing在广泛的writeup中描述了双重检查锁定的几种变体。他确实提到使用静态变量作为选项,他甚至表明编译器将生成用于双重检查锁定的代码以初始化静态变量。我不清楚他是否认为一种方式比另一种更好。
我觉得这两篇文章都是教学法,而且没有理由这样做。如果您使用静态变量或std :: call_once。
,编译器将为您完成答案 0 :(得分:22)
GCC使用特定于平台的技巧来完全避免快速路径上的原子操作,利用它可以比call_once或双重检查更好地分析static
。
因为双重检查使用原子作为避免种族案例的方法,所以每次都必须支付获得的价格。这不是一个高价,但它是一个价格。
它必须支付这一点,因为在所有情况下原子必须保持原子,甚至像比较交换这样的困难操作。这使得很难优化。一般来说,编译器必须保留它,以防万一你使用变量不仅仅是双锁。它没有简单的方法证明你永远不会在你的原子上使用一个更复杂的操作。
另一方面,static
是高度专业化的,也是语言的一部分。它从一开始就被设计为非常容易证明初始化。因此,编译器可以采用更通用版本不可用的快捷方式。 The compiler actually emits以下静态代码:
一个简单的功能:
void foo() {
static X x;
}
在GCC内重写为:
void foo() {
static X x;
static guard x_is_initialized;
if ( __cxa_guard_acquire(x_is_initialized) ) {
X::X();
x_is_initialized = true;
__cxa_guard_release(x_is_initialized);
}
}
这看起来很像双重检查锁。但是,编译器会在这里作弊。它知道用户永远不能直接使用cxa_guard
。它知道它仅用于编译器选择使用它的特殊情况。因此,利用这些额外信息,它可以节省一些时间。 CXA防护规范按原样分配,共享common rule:__cxa_guard_acquire
永远不会修改防护的第一个字节,__cxa_guard__release
会将其设置为非零。< / p>
这意味着每个守卫必须是单调的,并且它确切地指定了哪些操作将这样做。因此,它可以利用主机平台内的现有竞赛案例保护。例如,在x86上,由高度同步的CPU保证的LL / SS保护足以完成此获取/释放模式,因此它可以对该第一个字节进行原始读取。它是双重锁定,而不是获取 - 读取。这是唯一可能的,因为GCC没有使用C ++原子API进行双重锁定 - 它使用的是platform specific approach。
GCC无法在一般情况下优化原子。在设计为较少同步的架构(例如为1024+核设计的架构)中,GCC不依赖于该架构来为其执行LL / SS。因此,GCC被迫实际发射原子。但是,在x86和x64等常见平台上,它可以更快。
call_once
可以具有GCC静态效率,因为它同样限制了可以对once_flag
执行的操作数量,可以应用于原子的一小部分函数。权衡是静态在使用时更方便使用,但call_once
适用于静态不足的许多情况(例如动态生成的对象拥有的once_flag
)。
在这些更高的平台上,静态和call_once
之间的性能略有不同。许多这些平台虽然没有提供LL / SS,但至少会提供非整数的非撕裂读取。这些平台可以使用它和一个特定于线程的指针来执行per-thread epoch counting to avoid atomics。这对于静态或call_once
是足够的,但取决于计数器不会翻转。如果您没有无撕裂的64位整数,call_once
必须担心翻转。实施可能会或可能不会担心这一点。如果它忽略了这个问题,它可以像静态一样快。如果它注意到这个问题,它必须像原子一样慢。 Static在编译时知道有多少个静态变量/块,所以它可以证明在编译时没有翻转(或者至少是自信!)