提问人:k huang 提问时间:3/23/2023 更新时间:3/23/2023 访问量:61
闯入帮助程序函数是否有助于提高性能?
Can breaking into helper functions help performance?
问:
最近我很好奇为什么编译器并不总是内联每个函数。在我搜索后,我认为有趣的一个原因是,内联每个函数都会增加可执行文件的大小,并导致可能不适合缓存的更大函数。
但我很好奇,反过来是否适用。如果你有一个庞大的函数,那么该函数在编译后就有可能不适合缓存。那么,将函数分解为辅助函数是否真的对性能有好处呢?这让我觉得很有趣,因为我通常听到相反的声音,即闯入辅助函数确实会产生一些性能成本(当然,除非内联),我们只是接受更高的可读性的权衡。
答:
是的。它可以。当代码非常大时,这通常是一个好主意,内联函数不包含热循环,内联变体共享其代码的很大一部分,并且调用频率相当相同。在这种情况下,会有很多指令缓存未命中,从而大大减慢指令解码速度,从而减慢内联代码的执行速度。更大的代码会占用更多的缓存空间,并且由于可能的缓存丢失,也会导致更多的数据丢失。共享部分可以减少缓存中代码的空间,从而减少未命中的情况。在真正病态的情况下,代码可能非常大,以至于操作系统无法一次性加载所有页面,从而导致从内存绑定设备(例如嵌入式设备)上的存储设备重新加载页面。很少调用或在上下文切换后调用的冷代码通常应该很小,以便快速。除了少数情况外,代码很少会大量增长:模板实例化和半内联递归函数。
模板实例化可以快速生成大量代码,并且在多个实例之间共享重要部分的情况并不少见。共享代码不是免费的:非内联叶函数减小代码大小,但代价是函数调用可能在相对热的循环中,并且远跳转往往会导致缓存未命中,并且对于目标代码并不总是容易预测的(不可预测的跳转要昂贵得多)。
递归函数可以部分内联(几个级别),从而减轻函数调用的开销,并从一些优化步骤(如常量传播)中受益。如果代码变得太大,可以共享很少执行的分支并将其存储在很远的地方,以便代码可以更好地适应指令缓存(这对于热递归函数至关重要)。
主流编译器可以做这样的优化。例如,在 GCC 中:-ftree-tail-merge
查找相同的代码序列。找到后,将一个替换为跳转到另一个。这种优化称为尾部合并或交叉跳跃。默认情况下,此标志在 -O2 及更高版本处启用。可以使用 max-tail-merge-comparisons 参数和 max-tail-merge-iterations 参数限制此阶段中的编译时间。
例如,GCC 中也有标志:-fipa-icf
对函数和只读变量执行相同的代码折叠。该优化减小了代码大小,并且可能会通过将函数替换为具有不同名称的等效函数来干扰展开堆栈。启用链接时间优化后,优化工作效率更高。
请注意,您不需要将代码放入编译器可以共享相同部分的函数中,尽管这可以帮助它们(通常通过不内联叶调用,Clang 很容易做到这一点)。
相关文章: C 编译器会删除重复(合并)代码吗?
评论