gcc-c++ 是否没有为当前的 x86-64 处理器优化原子操作

Is gcc-c++ not optimizing atomic operations for current x86-64 processors

给定以下测试程序:

#include <atomic>
#include <iostream>

int64_t process_one() {
        int64_t a;
        //Should be atomic on my haswell
        int64_t assign = 42;
        a = assign;
        return a;
}

int64_t process_two() {
        std::atomic<int64_t> a;
        int64_t assign = 42;
        a = assign;
        return a;
}

int main() {
        auto res_one = process_one();
        auto res_two = process_two();
        std::cout << res_one << std::endl;
        std::cout << res_two << std::endl;
}

编译:

g++ --std=c++17 -O3 -march=native main.cpp

代码为这两个函数生成了以下汇编:

00000000004007c0 <_Z11process_onev>:
  4007c0:       b8 2a 00 00 00          mov    [=12=]x2a,%eax
  4007c5:       c3                      retq
  4007c6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4007cd:       00 00 00

00000000004007d0 <_Z11process_twov>:
  4007d0:       48 c7 44 24 f8 2a 00    movq   [=12=]x2a,-0x8(%rsp)
  4007d7:       00 00
  4007d9:       0f ae f0                mfence
  4007dc:       48 8b 44 24 f8          mov    -0x8(%rsp),%rax
  4007e1:       c3                      retq
  4007e2:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4007e9:       00 00 00
  4007ec:       0f 1f 40 00             nopl   0x0(%rax)

就我个人而言,我不太会说汇编程序,但是(我可能在这里弄错了)似乎 process_two 编译为包括所有 process_one,然后是一些。

然而,据我所知,'modern' x86-64 处理器(例如 Haswell,我在其上编译了这个)将自动赋值而不需要任何额外的操作(在这种情况下我相信额外的操作是 process_two).

中的 mfence 指令

那么为什么 gcc 不优化进程二中的代码以使其与进程一完全一样呢?鉴于我编译的标志。

是否仍然存在原子存储的行为不同于对普通变量赋值的情况,因为它们都在 8 字节上。

原因是默认使用 std::atomic 也意味着内存顺序

std::memory_order order = std::memory_order_seq_cst

为了实现这种一致性,编译器必须告诉处理器不要对指令重新排序。它通过使用 mfence 指令来实现。

改变你的

    a = assign;

    a.store(assign, std::memory_order_relaxed);

你的输出将从

process_two():
        mov     QWORD PTR [rsp-8], 42
        mfence
        mov     rax, QWORD PTR [rsp-8]
        ret

process_two():
        mov     QWORD PTR [rsp-8], 42
        mov     rax, QWORD PTR [rsp-8]
        ret

正如您所期望的那样。

只是错过了优化。例如,clang does just fine with it - 两个函数的编译结果与单个 mov eax, 42.

相同

现在,您必须深入研究 gcc 内部才能确定,但​​似乎 gcc 尚未实现许多围绕原子变量的常见和合法优化,包括合并连续的读取和写入。事实上,clangiccgcc 的 none 似乎优化了很多东西,除了 clang 处理局部原子(包括按值传递) 通过从本质上消除它们的原子性质,这在某些情况下很有用,例如通用代码。有时 icc 似乎生成特别糟糕的代码 - 参见 two_reads here,例如:它似乎只想使用 rax 作为地址 作为累加器,产生一连串 mov 指令,使事情变得混乱。

关于原子优化的一些更复杂的问题是 discussed here,我希望随着时间的推移编译器会在这方面做得更好。