【问题标题】:Do C++ compilers align small functions to optimize cache-line fetches?C++ 编译器是否对齐小函数以优化缓存行提取?
【发布时间】:2021-11-19 04:56:41
【问题描述】:

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

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

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

【问题讨论】:

  • 我会说这取决于编译器和优化设置。您应该分别研究每个编译器,因为在优化和优化级别方面没有标准。

标签: c++ assembly optimization compiler-optimization memory-alignment


【解决方案1】:

不,像 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 条目)。

【讨论】:

  • 是的,我的意思是字节,我解决了这个问题。感谢您的回复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-26
  • 2015-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多