0

考虑以下代码:

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)and add(a, 2.34)(对于同一个变量a),a 肯定会以 a+1.23、a+2.34 或 a+1.23+2.34 结束吗?

也就是说,这些结果中的一个肯定会在这次大会上发生,并且a不会以其他状态结束吗?

4

1 回答 1

3

这是我的一个相关问题:

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 或垃圾值


作为进一步说明,请记住,即使您的环境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 架构具有处理原子变量的特殊有效指令,而无需注入繁重的栅栏。这可能最终已经满足了您的性能要求。

于 2020-04-24T19:28:37.753 回答