考虑以下伪代码:
expected = null; if (variable == expected) { atomic_compare_exchange_strong( &variable,expected,desired(),memory_order_acq_rel,memory_order_acq); } return variable;
观察当执行变量==期望检查时,没有“获取”语义.
在我看来,希望将至少被称为一次,每次最多一次.
此外,如果需要,从不返回null,则此代码将永远不会返回null.
现在我有三个问题:
>以上是否正确?即,即使在每次阅读时都没有栅栏,我们也可以很好地阅读共享变量吗?
>可以在C中实现吗?如果是这样,怎么办?如果没有,为什么?
(希望有理由,不仅仅是因为标准这样说)
>如果(2)的答案为是,那么也可以在C中实现这一点,而不需要变量==期望执行变量的原子读取?
基本上,我的目标是要明白,一旦代码已被每个线程至少执行一次,那么可以以一种与非共享变量的性能相同的方式执行共享变量的延迟初始化?
(这有点是一个“语言律师”的问题,所以这意味着问题不在于这是一个好的还是有用的想法,而是在技术上是否可以正确地做到这一点.
解决方法
关于是否可以在C中执行共享变量的延迟初始化的问题,它具有与非共享变量的性能(几乎)相同的性能:
答案是,它取决于硬件架构,以及编译器和运行时环境的实现.至少在一些环境中是可能的.特别是在x86与GCC和Clang.
在x86上,可以在没有内存栅栏的情况下实现原子读取.基本上,原子阅读与非原子阅读相同.看看下面的编译单元:
std::atomic<int> global_value; int load_global_value() { return global_value.load(std::memory_order_seq_cst); }
虽然我使用了具有顺序一致性的原子操作(默认值),但生成的代码中没有什么特别的. GCC和Clang生成的汇编代码如下所示:
load_global_value(): movl global_value(%rip),%eax retq
我说几乎相同,因为还有其他原因可能会影响性能.例如:
尽管没有栅栏,但原子操作仍然阻止了一些编译器的优化,例如重新排序说明和消除商店和装载
>如果至少有一个线程写入到同一个高速缓存行上的另一个内存位置,它将对性能产生巨大的影响(称为虚假共享)
话虽如此,推荐的实现懒惰初始化的方式是使用std :: call_once.这将为您提供所有编译器,环境和目标体系结构的最佳结果.
std::once_flag _init; std::unique_ptr<gadget> _gadget; auto get_gadget() -> gadget& { std::call_once(_init,[this] { _gadget.reset(new gadget{...}); }); return *_gadget; }