我可能误解了缓存取取是如何工作的,但我很好奇是否有任何编译器优化来对那些没有内联的小函数。
如果给定机器上的缓存行大小为64字节,那么在单个缓存行中对小于64字节的函数指针是否合理,以防止多个缓存获取来检索该函数?
即使函数的大小是100个字节,它仍然可以对齐到两个缓存行,最坏的情况是3如果它是非对齐的。这是否是一个可行的优化,编译器是否在实际应用程序中使用这种方法,例如将常用的小函数打包在一起?
发布于 2021-09-27 00:34:12
不,像gcc和clang这样的主流编译器不会在缓存行开始时留下额外的未使用空间来启动一个小函数,以避免其结束跨越边界。他们也没有在文本部分中选择一个顺序,以便在不降低I-缓存和iTLB的总体代码密度的情况下对其进行优化。
AFAIK,GCC甚至不知道指令的大小,它是通过发出asm文本单独组装来编译的。在每个函数之前,它都使用.p2align 4 (假设默认情况是x86-64上的-falign-functions=16 ),或者clang -mllvm -align-all-functions=4 (2^4 = 16),因为CPU通常会获取这样大小的块,并且您希望第一个对齐的fetch能够提取多个有用的指令。
在函数内部,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 __builtin_expect()的示例。
有时,函数的后面这些部分在完成时会跳回函数的“主”路径,而其他时候,它们会独立地以自己的ret指令结束。(这被称为“尾复制”,通过复制尾声(如果有的话)来优化jmp。)
因此,如果您盲目地假设将整个函数放在同一个缓存行中很重要,但实际上在大多数调用中通常只执行前32个字节,那么最终您可能会取代一个更大一些的函数的开始,这样它就开始更接近缓存行的末尾,也许什么也得不到。
因此,这可能有助于配置文件引导的优化,以确定哪些函数实际上是热门的,并将它们彼此相邻(对于iTLB和L1i局部性)分组,并对它们进行排序,以便它们很好地打包。或者哪个函数往往是一起调用的,一个接一个。
相反,将经常被长期使用的函数组合在一起,这样这些缓存行就可以保持冷状态(如果有页面的话,甚至是iTLB条目)。
https://stackoverflow.com/questions/69339881
复制相似问题