不,像 gcc 和 clang 这样的主流编译器不会在缓存行的开头留出额外的未使用空间来启动一个小函数,以避免它的结尾越界。他们也不会在文本部分中选择一个在不降低 I-cache 和 iTLB 整体代码密度的情况下对此进行优化的顺序。
AFAIK,GCC 甚至不知道指令大小;它通过发出要单独组装的 asm 文本来编译。在每个函数之前,它使用.p2align 4(假设默认值是-falign-functions=16,就像在x86-64上一样),或者clang -mllvm -align-all-functions=4(2^4 = 16),因为CPU经常获取那个大小的块,并且你想要首先对齐提取以提取多个有用的指令。
在函数内部,GCC 默认对齐分支目标(或至少循环的顶部),如果需要 10 个或更少字节,则填充到下一个 16 的倍数,然后无条件对齐 8,但条件由汇编器(它知道机器码大小/位置):
.p2align 4,,10 # GCC does this for loop tops *inside* functions
.p2align 3
不过,有趣的想法可能值得研究这样做是否有任何实际好处。
大多数经常调用的函数在某些级别的缓存中已经很热(因为缓存可以工作,并且被频繁调用意味着它们往往会保持热状态),但这可能会减少缓存行的数量需要保持热度。
要考虑的另一件事是对于许多函数,并非所有代码字节都是热的。例如快速路径可能在前 32 个字节中,后面的代码字节仅是 if() 或 else 块,用于错误条件或其他特殊情况。 (找出通过函数的哪条路径是常见的路径是编译器工作的一部分,尽管配置文件引导优化 (PGO) 可以提供帮助。或者使用 C++20 [[likely]] / [[unlikely]] 进行提示可以达到相同的结果,如果提示实际上是正确的,让编译器对代码进行布局,以便快速路径最小化采用的分支并最大化缓存局部性。How do the likely/unlikely macros in the Linux kernel work and what is their benefit? 有一个使用 GNU C __builtin_expect() 的示例。
有时,函数的这些后面的部分在完成后会跳回函数的“主”路径,有时它们会以自己的 ret 指令独立结束。 (这称为“尾部复制”,如果有的话,通过复制结尾来优化 jmp。)
因此,如果您盲目地假设将整个函数放在同一缓存行中很重要,但实际上大多数调用通常只执行前 32 个字节,那么您最终可能会替换稍后更大函数的开头所以它开始接近缓存行的末尾,可能没有任何收获。
因此,通过配置文件引导的优化来确定哪些函数实际上是热的,并将它们彼此相邻分组(对于 iTLB 和 L1i 局部性),并对它们进行排序以便很好地打包,这可能会很好。或者哪些函数往往会一个接一个地被一起调用。
相反,将经常长时间未使用的函数组合在一起,这样这些缓存行就可以保持冷态(如果有它们的页面,甚至是 iTLB 条目)。