为什么错误共享仍然影响非原子,但远小于原子?
Why does false sharing still affect non atomics, but much less than atomics?
考虑以下证明虚假共享存在的例子:
using type = std::atomic<std::int64_t>;
struct alignas(128) shared_t
{
type a;
type b;
} sh;
struct not_shared_t
{
alignas(128) type a;
alignas(128) type b;
} not_sh;
一个线程递增 a
步长 1,另一个线程递增 b
。使用 MSVC 将增量编译为 lock xadd
,即使结果未被使用。
对于a
和b
分开的结构,not_shared_t
在几秒内累积的值大约是shared_t
的十倍。
到目前为止的预期结果:单独的缓存行在 L1d 缓存中保持热度,增加 lock xadd
吞吐量的瓶颈,错误共享是对缓存行进行 ping-ponging 的性能灾难。 (编者注:启用优化后,更高版本的 MSVC 版本使用 lock inc
。这可能会扩大竞争与非竞争之间的差距。)
现在我将 using type = std::atomic<std::int64_t>;
替换为普通的 std::int64_t
(非原子增量编译为 inc QWORD PTR [rcx]
。循环中的原子加载恰好阻止编译器将计数器保存在寄存器中直到循环退出。)
not_shared_t
的达到计数仍然大于 shared_t
,但现在不到两倍。
| type is | variables are | a= | b= |
|---------------------------|---------------|-------------|-------------|
| std::atomic<std::int64_t> | shared | 59’052’951| 59’052’951|
| std::atomic<std::int64_t> | not_shared | 417’814’523| 416’544’755|
| std::int64_t | shared | 949’827’195| 917’110’420|
| std::int64_t | not_shared |1’440’054’733|1’439’309’339|
为什么非原子情况在性能上如此接近?
下面是完成最小可重现示例的程序的其余部分。 (也On Godbolt with MSVC,准备compile/run)
std::atomic<bool> start, stop;
void thd(type* var)
{
while (!start) ;
while (!stop) (*var)++;
}
int main()
{
std::thread threads[] = {
std::thread( thd, &sh.a ), std::thread( thd, &sh.b ),
std::thread( thd, ¬_sh.a ), std::thread( thd, ¬_sh.b ),
};
start.store(true);
std::this_thread::sleep_for(std::chrono::seconds(2));
stop.store(true);
for (auto& thd : threads) thd.join();
std::cout
<< " shared: " << sh.a << ' ' << sh.b << '\n'
<< "not shared: " << not_sh.a << ' ' << not_sh.b << '\n';
}
非原子内存增量可以在重新加载自己的存储值时受益于存储转发。即使缓存行无效,也会发生这种情况。核心知道存储最终会发生,内存排序规则允许这个核心在它们变得全局可见之前看到它自己的存储。
Store-forwarding 给你存储缓冲区的长度,而不是 。
当这个核心最终获得缓存行的所有权时,它可以在 1/clock 提交多个存储。这比由内存目标增量创建的依赖链快 6 倍:~5 周期 store/reload 延迟 + 1 周期 ALU 延迟。 所以在非原子情况下,执行只是将新存储以 1/6 的速率放入 SB,而核心拥有它时这就是为什么没有大量存储的原因共享原子与非共享原子之间的差距。
当然也会有一些内存排序机器被清除; and/or SB full 是错误共享情况下吞吐量较低的可能原因。请参阅 上的答案和评论,了解另一个与此类似的实验。
A lock inc
或 lock xadd
强制存储缓冲区在操作之前耗尽,并且包括作为操作的一部分提交到 L1d 缓存。这使得存储转发变得不可能,并且只有在缓存行处于独占或修改的 MESI 状态时才会发生。
相关:
Can modern x86 implementations store-forward from more than one prior store?(不,但是那里的详细信息可能会帮助您准确了解存储缓冲区的作用以及存储转发如何在重新加载与存储完全重叠的情况下工作。)
考虑以下证明虚假共享存在的例子:
using type = std::atomic<std::int64_t>;
struct alignas(128) shared_t
{
type a;
type b;
} sh;
struct not_shared_t
{
alignas(128) type a;
alignas(128) type b;
} not_sh;
一个线程递增 a
步长 1,另一个线程递增 b
。使用 MSVC 将增量编译为 lock xadd
,即使结果未被使用。
对于a
和b
分开的结构,not_shared_t
在几秒内累积的值大约是shared_t
的十倍。
到目前为止的预期结果:单独的缓存行在 L1d 缓存中保持热度,增加 lock xadd
吞吐量的瓶颈,错误共享是对缓存行进行 ping-ponging 的性能灾难。 (编者注:启用优化后,更高版本的 MSVC 版本使用 lock inc
。这可能会扩大竞争与非竞争之间的差距。)
现在我将 using type = std::atomic<std::int64_t>;
替换为普通的 std::int64_t
(非原子增量编译为 inc QWORD PTR [rcx]
。循环中的原子加载恰好阻止编译器将计数器保存在寄存器中直到循环退出。)
not_shared_t
的达到计数仍然大于 shared_t
,但现在不到两倍。
| type is | variables are | a= | b= |
|---------------------------|---------------|-------------|-------------|
| std::atomic<std::int64_t> | shared | 59’052’951| 59’052’951|
| std::atomic<std::int64_t> | not_shared | 417’814’523| 416’544’755|
| std::int64_t | shared | 949’827’195| 917’110’420|
| std::int64_t | not_shared |1’440’054’733|1’439’309’339|
为什么非原子情况在性能上如此接近?
下面是完成最小可重现示例的程序的其余部分。 (也On Godbolt with MSVC,准备compile/run)
std::atomic<bool> start, stop;
void thd(type* var)
{
while (!start) ;
while (!stop) (*var)++;
}
int main()
{
std::thread threads[] = {
std::thread( thd, &sh.a ), std::thread( thd, &sh.b ),
std::thread( thd, ¬_sh.a ), std::thread( thd, ¬_sh.b ),
};
start.store(true);
std::this_thread::sleep_for(std::chrono::seconds(2));
stop.store(true);
for (auto& thd : threads) thd.join();
std::cout
<< " shared: " << sh.a << ' ' << sh.b << '\n'
<< "not shared: " << not_sh.a << ' ' << not_sh.b << '\n';
}
非原子内存增量可以在重新加载自己的存储值时受益于存储转发。即使缓存行无效,也会发生这种情况。核心知道存储最终会发生,内存排序规则允许这个核心在它们变得全局可见之前看到它自己的存储。
Store-forwarding 给你存储缓冲区的长度,而不是
当这个核心最终获得缓存行的所有权时,它可以在 1/clock 提交多个存储。这比由内存目标增量创建的依赖链快 6 倍:~5 周期 store/reload 延迟 + 1 周期 ALU 延迟。 所以在非原子情况下,执行只是将新存储以 1/6 的速率放入 SB,而核心拥有它时这就是为什么没有大量存储的原因共享原子与非共享原子之间的差距。
当然也会有一些内存排序机器被清除; and/or SB full 是错误共享情况下吞吐量较低的可能原因。请参阅
A lock inc
或 lock xadd
强制存储缓冲区在操作之前耗尽,并且包括作为操作的一部分提交到 L1d 缓存。这使得存储转发变得不可能,并且只有在缓存行处于独占或修改的 MESI 状态时才会发生。
相关:
Can modern x86 implementations store-forward from more than one prior store?(不,但是那里的详细信息可能会帮助您准确了解存储缓冲区的作用以及存储转发如何在重新加载与存储完全重叠的情况下工作。)