我需要使用atomic_ref来强制存储/加载顺序,并且由于我不能直接在产品的API标头中使用它(客户可以使用C++11),我想将它实现为我的DLL的函数。但在这种情况下,我不确定编译器是否足够聪明,不会围绕调用我的 DLL 函数重新排序存储/加载,该函数执行需要“释放”的存储。
因此,我尝试在 google 上搜索一些有关 dll 函数调用周围存储/加载重新排序的合法性的信息,并弹出了 David Vandevoorde 的以下答案:https://www.quora.com/Since-C-compilers-cannot-知道在外部翻译单元库中定义的函数是否包含内存屏障,直到链接时避免重新排序在此类函数周围读取写入来电
根据他的示例https://godbolt.org/z/YGrRcJ编译器可以自由地围绕函数调用重新排序存储和加载。
然后我假设以下代码也没有强制执行
producer
中的商店订单,此代码看起来更像是我想要在生产中使用的代码:
#include <atomic>
#include <cassert>
#include <thread>
bool flag = false;
int data = 0;
// Imagine functions from the following namespace are implemented in a separate dll
namespace defined_in_another_dll
{
void flag_ready(bool& b)
{
std::atomic_ref<bool>(b).store(true, std::memory_order_release);
}
bool is_ready(bool& b)
{
return std::atomic_ref<bool>(b).load(std::memory_order_acquire);
}
}
void producer()
{
data = 42;
defined_in_another_dll::flag_ready(flag); // This call may reorder with data assignment?
}
void consumer()
{
while (!defined_in_another_dll::is_ready(flag));
assert(data == 42); // May fire because of reordering in producer!
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
问题: 为什么编译器围绕函数调用重新排序赋值是合法的,而同时以下“生产者”函数应该是合法的?谁保证这个存储始终是内联的并且优化器可以看到编译器障碍?如果这个原子存储最终作为函数调用怎么办?这很令人困惑。
void producer()
{
data = 42;
std::atomic_ref<bool>(flag).store(true, std::memory_order_release);
}
为了符合 C++ 内存模型,编译器必须确保任何加载或存储可以被其他线程观察到不会通过内存屏障进行重新排序。 内存模型对函数调用边界无关,因此无论此类内存屏障是在当前函数中还是在它调用的另一个函数中,该规则都适用。 重要的是代码如何排序。
因此,如果编译器能够证明被调用的函数不执行任何内存屏障,也不观察自身的加载或存储,则编译器可能只会对函数调用之后的此类加载或存储进行重新排序。 只有当编译器能够在编译时(或链接时,如果是 LTO)实际看到被调用函数的整个源代码时,这才有可能。 如果函数是不透明的,例如在外部 DLL 中定义的函数肯定是这种情况,那么编译器必须假设它可能包含内存屏障,并且无法执行此类重新排序。
但是,这只适用于加载或存储可以被其他线程观察到。 在 David Vandevoorde 给出的示例中,变量
r
是调用者函数的本地变量,并且其地址不会传递到调用者之外(事实上,它的地址根本不会被获取)。 因此,(在 C++ 标准内)任何其他线程都无法知道此变量在内存中的位置,因此编译器推断没有其他线程可能观察到对此对象的任何加载或存储。
同样适用于在当前线程中执行的被调用函数
f
;它也无法访问r
。 因此,除了编译器当前正在编译的 r
本身的代码之外,任何代码都不会观察到对 g
的加载或存储。 因此,它可以随意重新排序此类加载或存储,无论是否存在任何内存屏障,只要 g
仍然表现出与按程序顺序执行相同的行为。 就像在本例中一样,它甚至可以完全优化它们;事实上,您可以看到 r
本身已被优化掉,并使用常量值 42
来代替它。