将整个函数(或函数的热点部分,即通过它的快速路径)保留在更少的缓存行中可减少 I-cache 占用空间。因此它可以减少缓存未命中的数量,包括在大部分缓存都处于冷态的启动时。在缓存行结束之前结束循环可能会给硬件预取时间以获取下一个。
访问 L1i 缓存中存在的任何行都需要相同的时间。 (除非您的缓存使用 way-prediction:这会引入“慢速命中”的可能性。有关该想法的提及和简要说明,请参见 these slides。显然 MIPS r10k 的 L2 缓存使用了它,并且Alpha 21264 的 L1 指令缓存也是如此,在其 2 路关联 64kiB L1i 中具有“分支目标”与“顺序”方式。或者查看任何在您使用 Google 搜索时出现的学术论文cache way prediction 就像我一样。)
除此之外,影响并不在于高速缓存行边界,而是超标量 CPU 中对齐的指令获取块。你是对的,影响不是来自你正在考虑的事情。
有关超标量(和无序)执行的介绍,请参阅 Modern Microprocessors
A 90-Minute Guide!。
许多超标量 CPU 使用对其 I-cache 的对齐访问来执行其第一阶段的指令获取。让我们通过考虑具有 4 字节指令宽度1 和 4 宽 fetch/decode/exec 的 RISC ISA 来简化。 (例如 MIPS r10k,虽然 IDK 如果我要弥补的其他一些东西完全反映了那个微架构)。
...
.top_of_loop:
insn1 ; at address 16*n + 12
; 16-byte boundary here
insn2 ; at address 16*n + 0
insn3 ; at address 16*n + 4
b .top_of_loop ; at address 16*n + 8
... after loop ; at address 16*n + 12
... after loop ; at address 16*n + 0
在没有任何类型的循环缓冲区的情况下,获取阶段每次执行时都必须从 I-cache 中获取循环指令。但这每次迭代至少需要 2 个周期,因为该循环跨越两个 16 字节对齐的提取块。它无法在一次未对齐的提取中提取 16 个字节的指令。
但是如果我们对齐循环的顶部,它可以在单个循环中获取,如果循环体没有其他瓶颈,则允许循环以 1 个循环/迭代运行。
...
nop ; at address 16*n + 12 ; NOP padding for alignment
.top_of_loop: ; 16-byte boundary here
insn1 ; at address 16*n + 0
insn2 ; at address 16*n + 4
insn3 ; at address 16*n + 8
b .top_of_loop ; at address 16*n + 12
... after loop ; at address 16*n + 0
... after loop ; at address 16*n + 4
对于不是 4 条指令的倍数的更大循环,仍然会在某处进行部分浪费的提取。不过,通常最好它不是循环的顶部。尽早将更多指令送入流水线有助于 CPU 发现和利用更多指令级并行性,以处理在指令获取上没有纯粹瓶颈的代码。
一般来说,将分支目标(包括函数入口点)对齐 16 个可能是一个胜利(代价是较低的代码密度带来的更大的 I-cache 压力)。如果您在 1 或 2 条指令内,一个有用的权衡可以是填充到 16 的下一个倍数。例如所以在最坏的情况下,一个 fetch 块至少包含 2 或 3 条有用的指令,而不仅仅是 1 条。
这就是 GNU 汇编器支持 .p2align 4,,8 的原因:如果距离 8 个字节或更近,则填充到下一个 2^4 边界。事实上,GCC 确实将该指令用于某些目标/架构,具体取决于调整选项/默认值。
在非循环分支的一般情况下,您也不想在缓存行的末尾附近跳转。那么你可能马上就会有另一个 I-cache 未命中。
脚注 1:
该原理也适用于具有可变宽度指令的现代 x86,至少当它们解码后的 uop 缓存未命中迫使它们实际从 L1I 缓存中获取 x86 机器代码时。并且适用于较旧的超标量 x86,例如 Pentium III 或 K8,没有 uop 缓存或环回缓冲区(无论对齐如何,都可以使循环高效)。
但是 x86 解码非常困难,需要多个流水线阶段,例如对于一些简单的find指令边界,然后将指令组提供给解码器。如果预解码可以赶上,只有初始提取块对齐,阶段之间的缓冲区可以隐藏解码器的气泡。
https://www.realworldtech.com/merom/4/ 展示了 Core2 前端的详细信息:16 字节获取块,与 PPro/PII/PIII 相同,馈送可扫描多达 32 字节并查找多达 6 条指令之间的边界的预解码阶段IIRC。然后将另一个缓冲区提供给完整的解码阶段,该阶段可以将多达 4 条指令(5 条带有 test 或 cmp + jcc 的宏融合)解码为多达 7 条微指令......
Agner Fog's microarch guide 提供了一些关于优化 x86 asm 以解决 Pentium Pro/II 与 Core2 / Nehalem 与 Sandybridge 系列以及 AMD K8/K10 与 Bulldozer 与 Ryzen 的提取/解码瓶颈的详细信息。
现代 x86 并不总是从对齐中受益。代码对齐会产生影响,但它们通常并不简单,也不总是有益的。事物的相对对齐可能很重要,但通常对于诸如哪些分支在分支预测器条目中相互别名,或者 uop 如何打包到 uop 缓存中。