C++ 编译器是否会调整小函数来优化缓存行获取?

G. *_*age 6 c++ optimization assembly memory-alignment compiler-optimization

我可能误解了缓存获取的工作原理,但我很好奇是否有任何编译器优化用于对齐未内联的小函数。

如果给定机器上的缓存行大小为 64 字节,那么将指向小于 64 字节的函数的函数指针在单个缓存行中对齐以防止多次缓存提取来检索该函数是否有意义?

即使函数大小为 100 字节,它仍然可以与 2 个缓存行对齐,如果未对齐,最坏的情况是 3 个缓存行。这是一种可行的优化吗?编译器是否在实际应用程序中使用类似的东西,例如将小型常用函数打包在一起?

Pet*_*des 4

不,像 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 默认情况下会通过填充到下一个 16 的倍数(如果需要 10 个或更少字节)来对齐分支目标(或至少是循环顶部),然后无条件地按 8 对齐,但条件是由汇编器实现的(其中确实知道机器代码大小/位置):

        .p2align 4,,10   # GCC does this for loop tops *inside* functions
        .p2align 3
Run Code Online (Sandbox Code Playgroud)

不过,有趣的想法可能值得研究一下这样做是否有任何现实好处。

最频繁调用的函数在某种级别的缓存中已经很热(因为缓存工作,并且被频繁调用意味着它们往往会保持热状态),但这可能会减少需要保持热状态的缓存行的数量。

另一件需要考虑的事情是,对于许多函数来说,并非所有代码字节都是 hot。例如,快速路径可能位于前 32 个字节中,后面的代码字节仅是if()错误else情况或其他特殊情况的块。(弄清楚函数的哪条路径是常见路径是编译器工作的一部分,尽管配置文件引导优化 (PGO) 可以提供帮助。或者,如果提示实际上正确,则使用 C++20 [[likely]]/进行提示可以实现相同的结果,[[unlikely]]让编译器布局代码,以便快速路径最小化所采用的分支并最大化缓存局部性 。Linux 内核中可能/不可能的宏如何工作以及它们的好处是什么?有一个使用 GNU C 的示例__builtin_expect()

有时,函数的这些后续部分在完成后会跳回函数的“主”路径,有时它们会独立地以自己的ret指令结束。(这称为“尾部复制”,jmp通过复制尾声(如果有)来优化 a。)

因此,如果您盲目地认为将整个函数放在同一个缓存行中很重要,但实际上大多数调用中通常只执行前 32 个字节,那么您最终可能会替换稍后更大的函数的开头,以便它开始接近缓存线的末尾,可能没有获得任何东西。

因此,通过配置文件引导的优化来找出哪些函数实际上是热门的,并将它们彼此相邻地分组(对于 iTLB 和 L1i 局部性),并对它们进行排序,以便它们很好地打包,这可能会很好。或者哪些函数倾向于被一个接一个地一起调用。

相反,将经常长时间不使用的功能分组在一起,以便这些缓存线可以保持冷状态(甚至是 iTLB 条目,如果有页面的话)。