考虑以下证明虚假共享存在的示例:
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 为增量递增,另一个线程以 1 递增b
。使用 MSVC编译增量lock xadd
,即使结果未使用。
对于a
和分开的结构,几秒钟内累积的值大约是for 的b
十倍。not_shared_t
shared_t
到目前为止的预期结果:单独的缓存线在 L1d 缓存中保持热,增加lock xadd
吞吐量瓶颈,错误共享是对缓存线的性能灾难。(编者注:lock inc
启用优化时使用更高版本的 MSVC。这可能会扩大竞争与非竞争之间的差距。)
现在我using type = std::atomic<std::int64_t>;
用普通代替std::int64_t
(非原子增量编译为inc QWORD PTR [rcx]
。循环中的原子负载恰好阻止编译器将计数器保留在寄存器中,直到循环退出。)
达到的计数not_shared_t
仍然大于 for 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|
为什么非原子情况在性能上如此接近?
这是完成最小可重现示例的程序的其余部分。(也在带有 MSVC 的 Godbolt 上,准备编译/运行)
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';
}