GCC在快速路径上使用特定于平台的技巧,完全避免原子操作,利用它可以更好地分析static
比call_once或双重检查。
因为双重检查使用原子作为避免竞争情况的方法,所以每次都要付出获取的代价。这不是一个高昂的代价,但它是一个代价。
它必须支付这个代价,因为原子必须在所有情况下保持原子性,即使是像比较交换这样困难的操作。这使得优化变得非常困难。一般来说,编译器必须将其留在代码中,以防你使用该变量进行更多的双重锁定之外的操作。它没有简单的方法来证明您从未在原子上使用其中更复杂的操作之一。
另一方面,static
是高度专业化的,并且是语言的一部分。从一开始,它就被设计成非常容易被证明初始化。因此,编译器可以采取通用版本不可用的快捷方式。实际上,编译器会发出以下静态代码:
一个简单的函数:
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保护规范分发时都共享一个
common rule:__cxa_guard_acquire不会修改保护的第一个字节,而__cxa_guard_release将其设置为非零值。
这意味着每个守卫都必须是单调的,并且明确指定了会使其单调的操作。因此,它可以利用主机平台内现有的竞态保护。例如,在x86上,强同步CPU所保证的LL/SS保护足以执行此获取/释放模式,因此在进行双重锁定时,它可以对该第一个字节进行
原始读取,而不是获取读取。这只有在GCC没有使用C++原子API来执行其双重锁定时才可能发生——它正在使用
特定于平台的方法。
在一般情况下,GCC无法优化掉原子性。在设计为较少同步的架构上(例如设计为1024+核心的架构),GCC不能依赖体系结构为其执行LL/SS。因此,GCC被迫实际发出原子操作。但是,在像x86和x64这样的常见平台上,它可能更快。
call_once
可以拥有与 GCC 的静态变量相同的效率,因为它同样将可以应用于原子操作的函数数量限制为仅一小部分可应用于 once_flag
的函数。这种权衡是,在适用时,静态变量更加方便使用,但在许多情况下,call_once
可以解决静态变量无法解决的问题(例如由动态生成的对象拥有的 once_flag
)。
在这些高级平台上,静态和
call_once
之间的性能略有差异。虽然这些平台没有提供LL/SS,但至少会提供整数的非撕裂读取。这些平台可以利用这一点和线程特定指针来进行
每个线程的时代计数以避免原子操作。这对于静态或
call_once
已经足够,但这取决于计数器不会翻转。如果您没有一个无撕裂的64位整数,
call_once
就必须担心翻转问题。实现可能会考虑这个问题,也可能不会。如果忽略此问题,它可以与静态变量一样快。如果注意到这个问题,它就必须像原子操作一样慢。静态知道在编译时有多少静态变量/块,因此它可以在编译时证明没有翻转(或至少非常自信!)