引言
在同一个线程中,如果指令之间没有前后的依赖关系,那么这些指令则在可能会会乱序执行。当时指令之间存在依赖关系,比如读写时,我们需要重点关注,因为这些指令如果乱序就会对代码逻辑产生可见的影响。
- 比如下的代码可能以任何数据执行,他们的执行顺序无法预测,因为他们之间没有依赖
int x = 1;
int y = 2;
int z = 3;
- 但是下面的代码将被串行执行,因为他们y的赋值依赖x的值,z 的值依赖 x 和 y
int x = fun1();
int y = fun2(x);
int z = x + y;
乱序的原因
产生指令执行顺序被打乱的原因有很多,比如编译器的优化、cpu 指令流水线的优化、cpu 数据核内写缓存cache 一致性限制、多个cpu间网络数据传递等。总之乱序的原因是为了提高程序执行的效率。这了我们要有一个概念 假设cpu 执行一条指令需要一个时钟周期,那么cpu 写L1存储也是一个周期,写L2大概10个周期,写内存大概需要100个周期,因此为了解决各个系统层级之间的读写速度差,cpu会出现大量的优化措施,这些就会导致指令的乱序执行。
编译器
编译优化手段有很多。比如常量折叠,无用代码消除等。
比如下面的例子
int main() {
int x = 1;
int y = 2;
int z = 3;
int a = y + z;
int b = a + 1;
return b;
}
如果采用 -O0 编译 gcc -g -c -O0 ./a.c && objdump -d -M intel -S ./a.o
生成汇编如下
int main() {
0: f3 0f 1e fa endbr64
4: 55 push rbp
5: 48 89 e5 mov rbp,rsp
int x = 1;
8: c7 45 ec 01 00 00 00 mov DWORD PTR [rbp-0x14],0x1
int y = 2;
f: c7 45 f0 02 00 00 00 mov DWORD PTR [rbp-0x10],0x2
int z = 3;
16: c7 45 f4 03 00 00 00 mov DWORD PTR [rbp-0xc],0x3
int a = y + z;
1d: 8b 55 f0 mov edx,DWORD PTR [rbp-0x10]
20: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
23: 01 d0 add eax,edx
25: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
int b = a + 1;
28: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
2b: 83 c0 01 add eax,0x1
2e: 89 45 fc mov DWORD PTR [rbp-0x4],eax
return b;
31: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
}
34: 5d pop rbp
35: c3 ret
但是如果采用-O2编译 gcc -g -c -O2 ./a.c && objdump -d -M intel -S ./a.o
则会生成如下汇编
int main() {
0: f3 0f 1e fa endbr64
int y = 2;
int z = 3;
int a = y + z;
int b = a + 1;
return b;
}
4: b8 06 00 00 00 mov eax,0x6
9: c3 ret
这个代码等价于
int main() {
return 6;
}
int main() {
0: f3 0f 1e fa endbr64
return 6;
}
4: b8 06 00 00 00 mov eax,0x6
9: c3 ret
可以看到不但代码顺序是可以打乱的,甚至连无用代码都可删除,常量可以在编译期计算。
编译优化有个前提,就是假设程序是单线程执行的,果编译器希望对程序指令的执行顺序做出改变,只要这些改变不影响该程序在单线程情况下的运行结果,那么这些改变就是允许的。
比如下面的代码 第6行和第7行的执行顺序就是无法保证的
#include
int main() {
int a = 10, b = 20, c = 30, d = 40, e = 50, f = 60, g = 70, h = 80;
int x = a + b + c + d;
int y = e + f + g + h;
std::cout