机器码布局优化(Machine Code Layout Optimizations)

CPU 前端(Frontend,FE)负责获取和解码指令,并将其传递给乱序后端(Backend,BE)。随着新一代处理器拥有更强的执行"算力",CPU FE 也需要足够强大,以保持整机的平衡。如果 FE 无法跟上指令供给的速度,BE 就会利用率不足,整体性能就会下降。这就是为什么 FE 被设计为始终领先于实际执行,以平滑任何可能发生的卡顿,并始终准备好待执行的指令。例如,2016 年发布的 Intel Skylake 每个周期最多可以获取 16 条指令。

大多数情况下,CPU FE 的低效可以描述为这样一种情况:后端正在等待指令执行,但前端无法及时提供。结果是 CPU 周期被浪费而没有完成任何实际有用的工作。回想一下,现代 CPU 每个周期可以处理多条指令,如今范围从 4 路到 9 路宽不等。并非所有可用槽位都被填满的情况非常频繁地发生。这对许多领域的应用程序(如数据库、编译器、网络浏览器等)来说是低效的根源。

TMA(Top-down Microarchitecture Analysis,自顶向下微架构分析)方法在 Frontend Bound(前端受限)指标中捕获 FE 性能问题。它表示 CPU FE 无法向 BE 传递指令(而 BE 本可以接受)的周期百分比。大多数真实应用程序的"Frontend Bound"指标不为零,这意味着一定比例的运行时间会因为次优的指令获取和解码而损失。低于 10\% 是正常的。如果你看到"Frontend Bound"指标超过 20\%,就值得花时间处理它。

FE 无法向执行单元传递指令的原因可能有很多。大多数时候,这是由于次优的代码布局,导致 I-cache(指令缓存)和 ITLB 利用率差。代码量庞大的应用程序(例如,数百万行代码)尤其容易出现 FE 性能问题。在本章中,我们将介绍一些改善机器码布局的典型优化方法。

机器码布局(Machine Code Layout)

当编译器将源代码翻译为机器码时,它会生成一个线性字节序列。代码清单 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 401136call bar()          │    401131 call baz
                │     jmp .label2         │    401136 ...
                │   .label1:              │
                │     call baz()          │
                │   .label2:              │
                │     ...                 │

代码在目标文件中的放置方式称为机器码布局。注意,对于同一个程序,可以用许多不同的方式排列代码。对于代码清单 MachineCodeLayout 中的代码,编译器可能决定反转分支,使得对 baz 的调用排在前面。此外,函数 barbaz 的函数体可以以两种不同的顺序放置:我们可以在可执行镜像中先放置 bar,然后放置 baz,也可以反过来。这会影响指令在内存中的偏移量,进而可能影响生成程序的性能,正如你稍后将看到的那样。在本章后续章节中,我们将介绍一些针对机器码布局的典型优化方法。

results matching ""

    No results matching ""