17.3 执行顺序
我们刚开始学 C 抽象机器的时候,习惯了按照 C 语句自然对应编译后的机器指令,两者有一致的顺序。现在是时候来深入细节,来了解为什么不完全是这样了。
一般我们会使用简单的方式来描述算法。但是由程序员所给出的指令顺序对于性能来说并不一定是最优的。
例如,编译器可能想要通过不修改代码语义的情况下来提升程序的局部性。列表 17-1 是一个例子:
Listing 17-1.ex_locality_src.c
char x[1000000], y[1000000];
...
x[4] = y[4];
x[10004] = y[10004];
x[5] = y[5];
x[10005] = y[10005];
列表 17-2 展示了一种可能的翻译结构。
Listing 17-2.ex_locality_asm1.asm
mov al,[rsi + 4]
mov [rdi+4],al
mov al,[rsi + 10004]
mov [rdi+10004],al
mov al,[rsi + 5]
mov [rdi+5],al
mov al,[rsi + 10005]
mov [rdi+10005],al
很明显,这些代码可以被重写以达到更好的局部性,首先赋值 x[4] 和 x[5],然后再赋值 x[10004] 和 x[10005],如列表 17-3 所示。
Listing 17-3.ex_locality_asm2.asm
mov al,[rsi + 4]
mov [rdi+4],al
mov al,[rsi + 5]
mov [rdi+5],al
mov al,[rsi + 10004]
mov [rdi+10004],al
mov al,[rsi + 10005]
mov [rdi+10005],al
如果抽象机器只考虑单个 CPU:给定初始状态,指令执行后的结果状态一致,那么这两种指令顺序的效果就是完全一样的。但第二种翻译结果性能上却更好,所以编译器倾向于选择第二种。这也是内存重重排序的一种简单场景:某些情况下内存访问顺序和源码中的顺序会有所区别。
对单线程应用来说,这些指令确实都是被 “真线性” 执行的,这种情况下我们可以认为操作的执行顺序对于可以观察到的行为没啥影响。但这种观念在进行多线程编程的时候就行不通了。
大多数没经验的程序员并不会太深入思考这个问题,早年间他们把自己限定在单线程编程的场景下,但近年我们已经没有办法不去考虑并行问题了。因为并行本身对我们的程序有侵入性,而且并行可以确确实实地提高程序性能。所以本章中,我们将讨论内存重排以及如何正确地使用内存重排。