从多个线程对双精度执行 += 的结果

Results of doing += on a double from multiple threads

考虑以下代码:

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 肯定会以 a+1.23、a+2.34 或a+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 位。

现在 XXXXWWWWVVVVPPPP 将表示与您预期的完全不同的浮点值。那是因为您最终混合了双精度变量的两个不同二进制表示形式 (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 or a rubbish value


进一步说明,请记住,即使您的环境 alignof(double) == 8 也不一定足以得出结论,您不会有错位问题。一切都取决于您的特定变量的来源。考虑以下(或 运行 它 here):

#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> 是实施工作和性能方面的最佳折衷方案实现线程安全。有 CPUs 架构具有特殊的高效指令来处理原子变量,而无需注入重围栏。这可能最终已经满足您的性能要求。