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
尚未实现许多围绕原子变量的常见和合法优化,包括合并连续的读取和写入。事实上,clang
、icc
或 gcc
的 none 似乎优化了很多东西,除了 clang
处理局部原子(包括按值传递) 通过从本质上消除它们的原子性质,这在某些情况下很有用,例如通用代码。有时 icc
似乎生成特别糟糕的代码 - 参见 two_reads
here,例如:它似乎只想使用 rax
作为地址 和 作为累加器,产生一连串 mov
指令,使事情变得混乱。
关于原子优化的一些更复杂的问题是 discussed here,我希望随着时间的推移编译器会在这方面做得更好。
给定以下测试程序:
#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
尚未实现许多围绕原子变量的常见和合法优化,包括合并连续的读取和写入。事实上,clang
、icc
或 gcc
的 none 似乎优化了很多东西,除了 clang
处理局部原子(包括按值传递) 通过从本质上消除它们的原子性质,这在某些情况下很有用,例如通用代码。有时 icc
似乎生成特别糟糕的代码 - 参见 two_reads
here,例如:它似乎只想使用 rax
作为地址 和 作为累加器,产生一连串 mov
指令,使事情变得混乱。
关于原子优化的一些更复杂的问题是 discussed here,我希望随着时间的推移编译器会在这方面做得更好。