x86 mfence 和 C++ 内存屏障
x86 mfence and C++ memory barrier
我正在检查编译器如何为 x86_64 上的多核内存屏障发出指令。下面的代码是我使用 gcc_x86_64_8.3
.
测试的代码
std::atomic<bool> flag {false};
int any_value {0};
void set()
{
any_value = 10;
flag.store(true, std::memory_order_release);
}
void get()
{
while (!flag.load(std::memory_order_acquire));
assert(any_value == 10);
}
int main()
{
std::thread a {set};
get();
a.join();
}
当我使用 std::memory_order_seq_cst
时,我可以看到 MFENCE
指令用于任何优化 -O1, -O2, -O3
。该指令确保存储缓冲区被刷新,因此更新它们在 L1D 缓存中的数据(并使用 MESI 协议确保其他线程可以看到效果)。
然而,当我使用没有优化的 std::memory_order_release/acquire
指令时,也使用了 MFENCE
指令,但是使用 -O1, -O2, -O3
优化省略了该指令,并且没有看到其他刷新缓冲区的指令。
在不使用MFENCE
的情况下,是什么确保将存储缓冲区数据提交到缓存内存以确保内存顺序语义?
下面是 -O3
函数的 get/set 的汇编代码,就像我们得到的 on the Godbolt compiler explorer:
set():
mov DWORD PTR any_value[rip], 10
mov BYTE PTR flag[rip], 1
ret
.LC0:
.string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
.string "any_value == 10"
get():
.L8:
movzx eax, BYTE PTR flag[rip]
test al, al
je .L8
cmp DWORD PTR any_value[rip], 10
jne .L15
ret
.L15:
push rax
mov ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
mov edx, 17
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
call __assert_fail
x86 内存排序模型为所有存储指令提供#StoreStore 和#LoadStore 屏障1,这是发布语义所需要的。处理器也会尽快提交一条存储指令;当存储指令退出时,存储成为存储缓冲区中最旧的存储,核心具有可写一致性状态的目标缓存行,并且缓存端口可用于执行存储操作2.所以不需要 MFENCE
指令。该标志将尽快对另一个线程可见,当它可见时,any_value
保证为 10。
另一方面,顺序一致性也需要#StoreLoad 和#LoadLoad 障碍。 MFENCE
需要同时提供 3 个障碍,因此它用于所有优化级别。
相关:.
脚注:
(1) 有些例外情况不适用于此处。特别是,非临时存储和不可缓存的写入组合内存类型的存储仅提供#LoadStore 屏障。无论如何,这些障碍是为存储到 Intel 和 AMD 处理器上的回写内存类型提供的。
(2) 这与在特定条件下全局可见的写组合存储相反。请参阅英特尔手册第 3 卷第 11.3.1 节。
(3) 请参阅 Peter 的回答下的讨论。
x86 的 TSO 内存模型是顺序一致性 + 存储缓冲区,因此只有 seq-cst 存储需要任何特殊的防护。(存储后停止,直到存储缓冲区耗尽,在以后加载之前,我们需要恢复顺序一致性)。较弱的 acq/rel 模型与存储缓冲区引起的 StoreLoad 重新排序兼容。
(请参阅评论中的讨论:"allowing StoreLoad reordering" 是否是对 x86 允许的内容的准确和充分的描述。内核始终按程序顺序查看其自己的存储,因为加载会监听存储缓冲区,因此您可以说存储转发还会重新排序最近存储的数据负载。除非你不能总是:)
(顺便说一句,gcc 以外的编译器使用 xchg
进行 seq-cst 存储。这在当前 CPU 上实际上 更高效 。GCC 的 mov
+mfence
过去可能更便宜,但即使您不关心旧值,目前通常更差。参见 for a comparison between GCC's mov+mfence
vs. xchg
. Also my answer on Which is a better write barrier on x86: lock+addl or xchgl?)
有趣的事实:您可以通过屏蔽 seq-cst loads 而不是存储来实现顺序一致性。但是对于大多数用例来说,便宜的负载比便宜的商店更有价值,所以每个人都使用 ABI,而商店的所有障碍都是如此。
见https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html for details of how C++11 atomic ops map to asm instruction sequences for x86, PowerPC, ARMv7, ARMv8, and Itanium. Also When are x86 LFENCE, SFENCE and MFENCE instructions required?
when I use std::memory_order_release/acquire with no optimizations MFENCE instruction is also used
那是因为flag.store(true, std::memory_order_release);
没有内联,因为你禁用了优化。这包括内联非常简单的成员函数,例如 atomic::store(T, std::memory_order = std::memory_order_seq_cst)
当 __atomic_store_n()
GCC 内置的排序参数是一个运行时变量(在 atomic::store()
头文件实现中),GCC保守地发挥它并将其提升到 seq_cst.
gcc 分支到 mfence
实际上可能是值得的,因为它太贵了,但这不是我们得到的。 (但这会使具有运行时变量顺序参数的函数的代码量更大,并且代码路径可能不会很热。因此,分支可能只是 libatomic 实现中的一个好主意,或者在极少数情况下使用配置文件引导的优化函数足够大,不能内联但采用可变顺序。)
我正在检查编译器如何为 x86_64 上的多核内存屏障发出指令。下面的代码是我使用 gcc_x86_64_8.3
.
std::atomic<bool> flag {false};
int any_value {0};
void set()
{
any_value = 10;
flag.store(true, std::memory_order_release);
}
void get()
{
while (!flag.load(std::memory_order_acquire));
assert(any_value == 10);
}
int main()
{
std::thread a {set};
get();
a.join();
}
当我使用 std::memory_order_seq_cst
时,我可以看到 MFENCE
指令用于任何优化 -O1, -O2, -O3
。该指令确保存储缓冲区被刷新,因此更新它们在 L1D 缓存中的数据(并使用 MESI 协议确保其他线程可以看到效果)。
然而,当我使用没有优化的 std::memory_order_release/acquire
指令时,也使用了 MFENCE
指令,但是使用 -O1, -O2, -O3
优化省略了该指令,并且没有看到其他刷新缓冲区的指令。
在不使用MFENCE
的情况下,是什么确保将存储缓冲区数据提交到缓存内存以确保内存顺序语义?
下面是 -O3
函数的 get/set 的汇编代码,就像我们得到的 on the Godbolt compiler explorer:
set():
mov DWORD PTR any_value[rip], 10
mov BYTE PTR flag[rip], 1
ret
.LC0:
.string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
.string "any_value == 10"
get():
.L8:
movzx eax, BYTE PTR flag[rip]
test al, al
je .L8
cmp DWORD PTR any_value[rip], 10
jne .L15
ret
.L15:
push rax
mov ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
mov edx, 17
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
call __assert_fail
x86 内存排序模型为所有存储指令提供#StoreStore 和#LoadStore 屏障1,这是发布语义所需要的。处理器也会尽快提交一条存储指令;当存储指令退出时,存储成为存储缓冲区中最旧的存储,核心具有可写一致性状态的目标缓存行,并且缓存端口可用于执行存储操作2.所以不需要 MFENCE
指令。该标志将尽快对另一个线程可见,当它可见时,any_value
保证为 10。
另一方面,顺序一致性也需要#StoreLoad 和#LoadLoad 障碍。 MFENCE
需要同时提供 3 个障碍,因此它用于所有优化级别。
相关:
脚注:
(1) 有些例外情况不适用于此处。特别是,非临时存储和不可缓存的写入组合内存类型的存储仅提供#LoadStore 屏障。无论如何,这些障碍是为存储到 Intel 和 AMD 处理器上的回写内存类型提供的。
(2) 这与在特定条件下全局可见的写组合存储相反。请参阅英特尔手册第 3 卷第 11.3.1 节。
(3) 请参阅 Peter 的回答下的讨论。
x86 的 TSO 内存模型是顺序一致性 + 存储缓冲区,因此只有 seq-cst 存储需要任何特殊的防护。(存储后停止,直到存储缓冲区耗尽,在以后加载之前,我们需要恢复顺序一致性)。较弱的 acq/rel 模型与存储缓冲区引起的 StoreLoad 重新排序兼容。
(请参阅评论中的讨论:"allowing StoreLoad reordering" 是否是对 x86 允许的内容的准确和充分的描述。内核始终按程序顺序查看其自己的存储,因为加载会监听存储缓冲区,因此您可以说存储转发还会重新排序最近存储的数据负载。除非你不能总是:
(顺便说一句,gcc 以外的编译器使用 xchg
进行 seq-cst 存储。这在当前 CPU 上实际上 更高效 。GCC 的 mov
+mfence
过去可能更便宜,但即使您不关心旧值,目前通常更差。参见 mov+mfence
vs. xchg
. Also my answer on Which is a better write barrier on x86: lock+addl or xchgl?)
有趣的事实:您可以通过屏蔽 seq-cst loads 而不是存储来实现顺序一致性。但是对于大多数用例来说,便宜的负载比便宜的商店更有价值,所以每个人都使用 ABI,而商店的所有障碍都是如此。
见https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html for details of how C++11 atomic ops map to asm instruction sequences for x86, PowerPC, ARMv7, ARMv8, and Itanium. Also When are x86 LFENCE, SFENCE and MFENCE instructions required?
when I use std::memory_order_release/acquire with no optimizations MFENCE instruction is also used
那是因为flag.store(true, std::memory_order_release);
没有内联,因为你禁用了优化。这包括内联非常简单的成员函数,例如 atomic::store(T, std::memory_order = std::memory_order_seq_cst)
当 __atomic_store_n()
GCC 内置的排序参数是一个运行时变量(在 atomic::store()
头文件实现中),GCC保守地发挥它并将其提升到 seq_cst.
gcc 分支到 mfence
实际上可能是值得的,因为它太贵了,但这不是我们得到的。 (但这会使具有运行时变量顺序参数的函数的代码量更大,并且代码路径可能不会很热。因此,分支可能只是 libatomic 实现中的一个好主意,或者在极少数情况下使用配置文件引导的优化函数足够大,不能内联但采用可变顺序。)