对于线程安全延迟初始化,应该选择函数、std::call_once或显式双重检查锁定中的静态变量吗?有什么有意义的区别吗?
这三个问题都可以从这个问题中看出。
两个版本的双重检查锁定在C++11出现在谷歌。
Anthony Williams 显示同时使用显式内存排序和std::call_once进行双重检查锁定。他没有提到静态,但这篇文章可能是在C++11编译器可用之前编写的。
杰夫普雷辛,在一个广泛的记记,描述了几个变化的双重检查锁定。他确实提到了使用静态变量作为选项,他甚至显示编译器将生成用于双重检查锁定的代码来初始化静态变量。我不清楚他是否认为一种方法比另一种方法更好。
我觉得这两篇文章都是教学性的,没有理由这么做。如果使用静态变量或std::call_once,编译器将为您执行此操作。
发布于 2014-11-29 20:16:06
GCC使用特定平台的技巧,以避免原子操作完全在快速路径,利用这一事实,它可以更好地分析static,而不是call_once或复核。
因为双重检查使用atomics作为避免竞争案例的方法,所以每次都要付出收购的代价。这不是一个很高的价格,但它是一个价格。
它必须支付这个费用,因为atomics在所有情况下都必须保持原子化,甚至是像比较交换这样的困难操作。这使得优化变得非常困难。一般来说,编译器必须把它放在里面,以防你只使用这个变量而不仅仅是双锁。它没有简单的方法来证明你从未对原子使用过更复杂的操作之一。
另一方面,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_guard。它知道它只在编译器选择使用它的特殊情况下使用。因此,有了这些额外的信息,它可以节省一些时间。CXA保护规范(尽管它们是分布式的)都共享一个共同规则:__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 ),它可以工作。
在这些较高的平台上,静态和call_once之间的性能略有差异。许多这样的平台,虽然不提供LL/SS,至少将提供一个整数的不撕裂读取。这些平台可以使用这个和线程特定的指针来执行每线程周期计数以避免原子化.这对于静态或call_once来说是足够的,但取决于计数器,而不是滚动。如果您没有无撕裂的64位整数,call_once必须担心滚动。实现可能担心也可能不担心这一点。如果它忽视了这个问题,它的速度可能和静力学一样快。如果它关注这个问题,它必须像原子一样慢。静态知道在编译时有多少静态变量/块,所以它可以证明编译时没有滚动(或者至少是非常自信!)
https://stackoverflow.com/questions/26013650
复制相似问题