机器代码布局优化
CPU 前端(FE)负责获取和解码指令,并将它们传递给乱序执行的后端。随着新型处理器获得更多的执行"马力(horsepower)",CPU FE 需要足够强大以保持机器平衡。如果 FE 无法跟上指令供应,后端将被低效利用,整体性能将受到影响。这就是为什么 FE 设计为始终领先于实际执行,以平滑可能出现的任何问题(hiccup),并始终准备好待执行的指令。例如,2016年发布的英特尔 Skylake 可以在每个周期内获取多达16条指令。
大多数时候,CPU FE 的效率低下可以描述为后端等待执行指令,但 FE 无法提供它们的情况。结果,CPU 周期在没有进行任何实际有用工作的情况下被浪费。回想一下,现代 CPU 每个周期可以处理多条指令,现在范围从4- 到 8-wide。不是所有可用插槽都被填满的情况经常发生。这代表了数据库、编译器、网络浏览器等多个领域应用程序的效率低下来源。
TMA 方法论通过 前端瓶颈(Front-End Bound)
指标捕捉 FE 性能问题。它代表了 CPU FE 无法向 BE 提供指令的周期百分比,而它本可以接受它们。大多数实际应用程序都会经历非零的Front-End Bound
指标,这意味着一定百分比的运行时间将因次优的指令获取和解码而损失。低于10%是常态。如果你看到"Front-End Bound"指标超过20%,那么绝对值得花时间解决它。
FE 无法向执行单元提供指令的原因可能有很多。大多数时候,这是由于代码布局不佳,导致 I-cache 和 ITLB 利用不佳。拥有大型代码库的应用程序,例如数百万行代码,特别容易受到 FE 性能问题的影响。在本章中,我们将探讨一些典型的优化措施,以改善机器代码布局并提高程序的整体性能。
机器代码布局
当编译器将源代码转换为机器代码时,它会生成一个线性的字节序列。@lst:MachineCodeLayout 显示了一个小段 C++ 代码的二进制布局示例。一旦编译器完成生成汇编指令,它需要对它们进行编码并按顺序排列在内存中。
代码清单:机器代码布局示例
C++ Code Assembly Listing Disassembled Machine Code
........ ................ .........................
if (a <= b) │ ; a is in edi │ 401125 cmp esi, edi
bar(); │ ; b is in esi │ 401128 jb 401131
else │ cmp esi, edi │ 40112a call bar
baz(); │ jb .label1 │ 40112f jmp 401136
│ call bar() │ 401131 call baz
│ jmp .label2 │ 401136 ...
│ .label1: │
│ call baz() │
│ .label2: │
│ ... │
代码在二进制文件中放置的方式称为 机器代码布局。请注意,对于同一个程序,可以以许多不同的方式布局代码。对于 @lst:MachineCodeLayout 中的代码,编译器可能决定反转分支,以便首先调用baz
。此外,函数bar
和baz
的主体可以以两种不同的顺序放置:我们可以先在二进制文件中放置bar
,然后放置baz
,或者反转顺序。这会影响指令在内存中放置的偏移量,进而可能会影响生成的二进制文件的性能,正如您将在后面看到的。在本章的以下部分中,我们将介绍一些机器代码布局的典型优化。