鉴于以下测试计划:
CreteInstance
编译:
#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;
}
代码为这两个函数生成了以下asm:
g++ --std=c++17 -O3 -march=native main.cpp
就个人而言,我不会说很多汇编语言但是(我可能会在这里弄错)似乎process_two编译为包含所有process_one&,然后是一些。
然而,据我所知,&#39;现代&#39; x86-64处理器(例如Haswell,我编译它)将自动进行分配而不需要任何额外的操作(在这种情况下,我认为额外的操作是process_two中的00000000004007c0 <_Z11process_onev>:
4007c0: b8 2a 00 00 00 mov $0x2a,%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 $0x2a,-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)
指令。)
那么为什么gcc只是优化第二个过程中的代码来完成与过程一样的情况呢?鉴于我编译的标志。
是否存在原子存储的行为与正常变量的赋值不同的情况,因为它们都是8个字节。
答案 0 :(得分:10)
原因是默认使用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
就像你预期的那样。
答案 1 :(得分:2)
这只是错过了优化。例如,clang does just fine with it - 两个函数编译为单个gcc
。
现在,您必须深入研究gcc
内部,但似乎clang
尚未实现围绕原子变量的许多常见和法律优化,包括合并连续读写。事实上,icc
,gcc
或clang
似乎没有优化任何东西,除了icc
通过基本上删除处理局部原子(包括传递值)它们的原子性质,在某些情况下很有用,例如通用代码。有时two_reads
似乎会产生特别糟糕的代码 - 请参阅rax
here,例如:它似乎只想使用mov
作为地址和作为累加器,导致一系列state
指令随机移动。
围绕原子优化的一些更复杂的问题是discussed here,我希望编译器会随着时间的推移而变得更好。