gpt4 book ai didi

c++ - 我如何证明 volatile 分配不是原子的?

转载 作者:行者123 更新时间:2023-12-01 12:53:13 26 4
gpt4 key购买 nike

我知道在C++中赋值可能不是原子的。
我试图触发一个竞赛条件来显示这一点。

但是,我的以下代码似乎没有触发任何此类事件。
我如何更改它以使其最终触发比赛条件?

#include <iostream>
#include <thread>

volatile uint64_t sharedValue = 1;
const uint64_t value1 = 13;
const uint64_t value2 = 1414;

void write() {
bool even = true;
for (;;) {
uint64_t value;
if (even)
value = value1;
else
value = value2;
sharedValue = value;
even = !even;
}
}

void read() {
for (;;) {
uint64_t value = sharedValue;
if (value != value1 && value != value2) {
std::cout << "Race condition! Value: " << value << std::endl << std::flush;
}
}
}

int main()
{
std::thread t1(write);
std::thread t2(read);
t1.join();
}

我正在使用VS 2017并在x86版本中进行编译。

这是作业的分解:
sharedValue = value;
00D54AF2 mov eax,dword ptr [ebp-18h]
00D54AF5 mov dword ptr [sharedValue (0D5F000h)],eax
00D54AFA mov ecx,dword ptr [ebp-14h]
00D54AFD mov dword ptr ds:[0D5F004h],ecx

我想这意味着分配不是原子的?
似乎有32位被复制到通用32位寄存器eax中,而其他32位又被复制到另一个通用32位寄存器ecx中,然后再复制到数据段寄存器中的 sharedValue中?

我还尝试了 uint32_t,一次就复制了所有数据。
因此,我猜想在x86上是否不需要对32位数据类型使用 std::atomic

最佳答案

一些答案/评论建议在作家中 sleep 。这是没有用的。您想要的是尽可能快地更改高速缓存行。 (以及volatile分配和读取的结果。)当对高速缓存行的MESI共享请求到达写入器核心(介于将存储的两半从存储缓冲区提交到L1d高速缓存之间)时,分配将被破坏。

如果您 sleep 了,那么您将等待很长时间,而没有创建一个窗口来实现此目的。在两半之间 sleep 会使检测起来更加容易,但是除非您使用单独的memcpy编写64位整数等的两半,否则您将无法做到这一点。

即使写入是原子的,也可以在读取器中的两次读取之间进行撕裂。这可能不太可能,但是在实践中仍然会发生很多。现代的x86 CPU可以在每个时钟周期执行两次加载(自Sandybridge以来为Intel,自K8以来为AMD)。我使用原子64位存储进行了测试,但是在Skylake上拆分了32位负载,并且撕裂仍然很频繁,足以在终端机中弹出几行文本。因此,CPU不能以总是在同一时钟周期内执行相应读取对的方式,以锁步方式运行所有内容。因此,有一个窗口供读取器使一对缓存之间的缓存行无效。 (但是,当缓存行由写入器核心拥有时,所有未决的缓存未命中加载可能在缓存行到达时立即全部完成。可用的加载缓冲区总数在现有的微体系结构中为偶数。)

如您所见,您的测试值都具有0 的上半部分,因此无法观察到任何撕裂。只有32位对齐的下半部分一直在变化,并且在原子上发生了变化,因为您的编译器保证uint64_t的至少4字节对齐,而x86保证4字节对齐的加载/存储是原子的。
0-1ULL是显而易见的选择。对于64位结构,我在this GCC C11 _Atomic bug的测试用例中使用了相同的内容。

对于您的情况,我会这样做。 read()write()是POSIX系统调用名称,因此我选择了其他名称。

#include <cstdint>
volatile uint64_t sharedValue = 0; // initializer = one of the 2 values!

void writer() {
for (;;) {
sharedValue = 0;
sharedValue = -1ULL; // unrolling is vastly simpler than an if
}
}

void reader() {
for (;;) {
uint64_t val = sharedValue;
uint32_t low = val, high = val>>32;
if (low != high) {
std::cout << "Tearing! Value: " << std::hex << val << '\n';
}
}
}

MSVC 19.24 -O2将编写器编译为使用= =的 movlpd 64位存储,但为 -1使用两个单独的 = -1 32位存储。 (和读取器分别加载两个32位)。正如您所期望的那样,GCC在编写器中总共使用了四个 mov dword ptr [mem], imm32存储。 ( Godbolt compiler explorer)

术语:它始终是竞争条件(即使具有原子性,您也不知道要获取两个值中的哪个)。使用 std::atomic<>,您将只具有该花园品种的竞赛条件,而没有未定义的行为。

问题是,对于特定平台,在特定的C++实现/一组编译选项上,是否确实会看到 volatile对象上的数据争用未定义行为(Undefined Behavior)。 数据争用UB是一个技术术语,比“种族条件” 具有更具体的含义。我更改了错误消息,以报告我们检查的一种症状。请注意,非 volatile对象上的数据争用UB可能具有怪异的效果,例如托管加载或循环外存储,甚至发明了额外的读取操作,导致代码认为一次读取同时为真和为假。 ( https://lwn.net/Articles/793253/)

我删除了2个冗余的cout刷新:一个来自 std::endl,另一个来自 std::flush。 cout默认为行缓冲,如果写入文件则为全缓冲,这很好。就DOS行尾而言, '\n'std::endl一样可移植。文本与二进制流模式可以解决此问题。 endl仍然只是 \n

我通过检查high_half == low_half 简化了撕裂检查。然后,编译器只需要发出一个cmp/jcc即可,而不是发出两个扩展精度比较,以查看该值是0还是-1。我们知道,在x86(或任何其他运行正常的编译器的主流ISA)上没有像 high = low = 0xff00ff00这样的假阴性的合理方法。

So I guess that on x86 there is no need to use std::atomic for 32 bit data types?



不正确的

使用 volatile int手动滚动原子不能给您原子RMW操作(没有内联asm或Windows InterlockedIncrement或GNU C内置 __atomic_fetch_add内置的特殊功能),也不能给您任何排序保证。其他代码。 (发布/获取语义)

When to use volatile with multi threading?-几乎永远不会。

仍然可以使用 volatile滚动您自己的原子,并且事实上已经得到许多主流编译器的支持(例如,Linux内核以及内联asm仍然可以这样做)。实际的编译器确实可以有效地定义 volatile对象上的数据争用行为。但是,如果有一种便携式且可保证安全的方法,通常是个坏主意。只需将 std::atomic<T>std::memory_order_relaxed一起使用即可获得与oj​​it_code一样高效的asm(对于 volatile可以使用的情况),但是可以保证ISO C++标准的安全性和正确性。
volatile还允许您使用C++ 17 atomic<T> 或更旧的成员函数来询问实现是否可以廉价地实现原子类型。 (在实践中,C++ 11实现决定不让任何给定原子的实例但并非所有实例基于对齐方式或某些事物而被锁定;相反,它们只是给atom原子提供所需的alignas(如果存在)。因此,C++ 17实现了一个常量(按类型常量),而不是按对象成员函数检查锁自由的方法)。

std::atomic<T>::is_always_lock_free还可以为比普通寄存器宽的类型提供便宜的无锁原子性。例如在ARM上,使用ARMv6的 std::atomic/ strd来存储/加载一对寄存器。

在32位x86上,优秀的编译器可以通过使用SSE2 ldrd进行原子的64位加载和存储来实现 std::atomic<uint64_t>,而无需使用non-lock_free机制(锁表)。 实际上,GCC和clang9确实使用movq加载/存储。不幸的是,clang8.0和更早版本使用 movq。 MSVC以更低效的方式使用 atomic<uint64_t>。在Godbolt链接中更改sharedVariable的定义以查看它。 (或者,如果您在循环中分别使用默认的seq_cst和 lock cmpxchg8b存储库中的一个,则MSVC由于某种原因会为其中一个调用 lock cmpxchg8b helper函数。但是,当两个存储库具有相同的顺序时,它将内联锁cmpxchg8b的循环比clang8更笨拙.0)请注意,这种效率低下的MSVC代码源是针对 memory_order_relaxed不是原子的;在这种情况下,带有 ?store@?$_Atomic_storage@_K$07@std@@QAEX_KW4memory_order@2@@Zvolatile也可以很好地编译。

通常,您无法从 atomic<T>获得宽原子的代码源。尽管GCC实际上确实将movq用于if()bool写函数(请参见前面的Godbolt编译器资源管理器链接),因为它看不到交替或其他东西。它还取决于您使用什么值。使用0和-1时,它将使用单独的32位存储,但是使用0和 mo_relaxed时,您会得到movq来表示可用的模式。 (我用它来验证您仍然可以从读取端撕开,而不是手写一些asm。)我的简单展开版本编译为仅将简单的 volatile存储与GCC一起使用。这是一个很好的例子,说明在这种详细程度下 0x0f0f0f0f0f0f0f0fULL的真正编译方式为零。

mov dword [mem], imm32也将保证原子对象的8字节对齐,即使普通volatile可能只对齐了4字节。

在ISO C++中,对atomic<uint64_t>对象的数据争夺仍然是未定义的行为。 (使用信号处理程序的 uint64_t竞赛除外。)

“数据竞赛”是指发生两个不同步的访问并且它们都不都是读取时的情况。 ISO C++允许在带有硬件竞争检测之类的机器上运行;实际上,没有主流系统能够做到这一点,因此,如果 volatile 对象不是“自然原子”的,结果只会令人眼花tear乱。

从理论上讲,ISO C++允许在没有一致性共享内存并且需要在原子存储后进行手动刷新的机器上运行,但这在实践中并不是很合理。 AFAIK没有现实世界中的实现。内核具有非一致性共享内存的系统(例如某些具有DSP内核+微 Controller 内核的ARM SoC)不会在这些内核上启动std::thread。

另请参阅 Why is integer assignment on a naturally aligned variable atomic on x86?

即使您在实践中没有观察到撕裂,它仍然是UB,,尽管正如我所说的那样,实际的编译器确实定义了volatile的行为。

Skylake实验以尝试检测存储缓冲区合并

我想知道存储缓冲区中的存储合并是否可以从两个单独的32位存储中创建对L1d高速缓存的原子64位提交。 (到目前为止,没有有用的结果,如果有人有兴趣或想在此基础上继续,请把它留在这里。)

我为读者使用了GNU C __atomic内置函数,因此,如果商店最终还是原子的,我们也不会流泪。
void reader() {
for (;;) {
uint64_t val = __atomic_load_n(&sharedValue, __ATOMIC_ACQUIRE);
uint32_t low = val, high = val>>32;
if (low != high) {
std::cout << "Tearing! Value: " << std::hex << val << '\n';
}
}
}

这是使微建筑对商店进行分组的一种尝试。
void writer() {
volatile int separator; // in a different cache line, has to commit separately
for (;;) {
sharedValue = 0;

_mm_mfence();
separator = 1234;
_mm_mfence();
sharedValue = -1ULL; // unrolling is vastly simpler than an if

_mm_mfence();
separator = 1234;
_mm_mfence();
}
}

我仍然看到这种眼泪。 (Skylake上的 volatile具有更新的微代码,就像 volatile sig_atomic_t一样,它会阻止乱序的exec并耗尽存储缓冲区。因此,以后的存储甚至不应该在后面的存储离开之前进入存储缓冲区。这实际上可能是一个问题,因为我们需要时间进行合并,而不仅仅是在存储区退休后尽快“毕业”就提交32位存储区)。

大概我应该尝试测量撕裂的速度,看看它是否频频出现,因为任何撕裂都足以在4GHz机器上向终端窗口发送文本垃圾邮件。

关于c++ - 我如何证明 volatile 分配不是原子的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60464359/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com