考虑以下代码:
void add(double& a, double b) {
a += b;
}
根据godbolt在Skylake上编译为:
add(double&, double):
vaddsd xmm0, xmm0, QWORD PTR [rdi]
vmovsd QWORD PTR [rdi], xmm0
ret
如果我从不同的线程调用add(a, 1.23)
和add(a, 2.34)
(对于相同的变量a
(,a肯定会以+1.23、+2.34或+1.23+2.34结束吗?
也就是说,给定这个程序集,这些结果中的一个肯定会发生吗,而a
不会最终处于其他状态?
这里有一个与我相关的问题:
CPU是否获取您在单个操作中处理的单词
某些处理器可能会通过一个接一个地进行两次提取(当然是非原子性的(来允许内存访问内存中碰巧未对齐的变量。
在这种情况下,如果另一个线程在第一个线程已经获取单词的第一部分时插入对该内存区域的写入,然后在另一个螺纹已经修改单词时获取第二部分,则会出现问题。
thread 1 fetches first part of a XXXX
thread 1 fetches second part of a YYYY
thread 2 fetches first part of a XXXX
thread 1 increments double represented as XXXXYYYY that becomes ZZZZWWWW by adding b
thread 1 writes back in memory ZZZZ
thread 1 writes back in memory WWWW
thread 2 fetches second part of a that is now WWWW
thread 2 increments double represented as XXXXWWWW that becomes VVVVPPPP by adding b
thread 2 writes back in memory VVVV
thread 2 writes back in memory PPPP
为了保持紧凑,我用一个字符表示8位。
现在XXXXWWWW
和VVVVPPPP
将表示与您预期的完全不同的浮点值。这是因为您最终混合了双变量的两种不同二进制表示(IEEE-754(的两部分。
话虽如此,我知道在某些基于ARM的架构中,数据访问是不允许的(这会导致陷阱的生成(,但我怀疑英特尔处理器确实允许这样做。
因此,如果你的变量a
对齐,你的结果可以是中的任何一个
a+1.23,a+2.34,a+1.23+2.34
如果您的变量可能对齐错误(即地址不是8的倍数(,则结果可能是
a+1.23、a+2.34、a+1.23+2.34或a垃圾值
作为进一步的注意事项,请记住,即使您的环境为alignof(double) == 8
,也不一定足以得出您不会出现错位问题的结论。这一切都取决于你的特定变量来自哪里。考虑以下内容(或在此处运行(:
#pragma push()
#pragma pack(1)
struct Packet
{
unsigned char val1;
unsigned char val2;
double val3;
unsigned char val4;
unsigned char val5;
};
#pragma pop()
int main()
{
static_assert(alignof(double) == 8);
double d;
add(d,1.23); // your a parameter is aligned
Packet p;
add(p.val3,1.23); // your a parameter is now NOT aligned
return 0;
}
因此,断言alignof()
并不一定能保证变量对齐。如果你的变量没有涉及任何包装,那么你应该没事。
请允许我为阅读此答案的其他人提供免责声明:在这些情况下使用std::atomic<double>
是实现线程安全的最佳折衷方案。有些CPU体系结构具有特殊的高效指令,用于处理原子变量,而无需注入沉重的围栏。这可能最终已经满足了您的性能要求。