C++ 编译器是否对齐小函数以优化缓存行提取?
Do C++ compilers align small functions to optimize cache-line fetches?
我可能误解了缓存提取的工作方式,但我很好奇是否有任何编译器优化来对齐非内联的小函数。
如果给定机器上的缓存行大小为 64 字节,将指向小于 64 字节的函数的函数指针在单个缓存行中对齐以防止多次缓存提取以检索是否有意义函数?
即使函数大小为 100 字节,它仍然可以对齐到 2 个缓存行,如果未对齐,最坏的情况是 3 个。这是一个可行的优化吗?编译器是否在实际应用程序中使用类似的东西,例如将小的常用函数打包在一起?
不,像 gcc 和 clang 这样的主流编译器不会留下多余的未使用 space 来在缓存行的开头启动一个小函数,以避免其结尾越界。他们也不会在不降低 I-cache 和 iTLB 整体代码密度的情况下,在文本部分选择一个优化顺序。
据我所知,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
不过,有趣的想法可能值得研究这样做是否有任何实际好处。
最常调用的函数在某些级别的缓存中已经很热(因为缓存工作,并且被频繁调用意味着它们往往会保持热),但这可能会减少需要保持热的缓存行数。
另一件需要考虑的事情是,对于许多函数,并不是所有的代码字节都是热的。例如快速路径可能在前 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 条目)。
我可能误解了缓存提取的工作方式,但我很好奇是否有任何编译器优化来对齐非内联的小函数。
如果给定机器上的缓存行大小为 64 字节,将指向小于 64 字节的函数的函数指针在单个缓存行中对齐以防止多次缓存提取以检索是否有意义函数?
即使函数大小为 100 字节,它仍然可以对齐到 2 个缓存行,如果未对齐,最坏的情况是 3 个。这是一个可行的优化吗?编译器是否在实际应用程序中使用类似的东西,例如将小的常用函数打包在一起?
不,像 gcc 和 clang 这样的主流编译器不会留下多余的未使用 space 来在缓存行的开头启动一个小函数,以避免其结尾越界。他们也不会在不降低 I-cache 和 iTLB 整体代码密度的情况下,在文本部分选择一个优化顺序。
据我所知,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
不过,有趣的想法可能值得研究这样做是否有任何实际好处。
最常调用的函数在某些级别的缓存中已经很热(因为缓存工作,并且被频繁调用意味着它们往往会保持热),但这可能会减少需要保持热的缓存行数。
另一件需要考虑的事情是,对于许多函数,并不是所有的代码字节都是热的。例如快速路径可能在前 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 条目)。