考虑到您只是在尝试优化速度,决定是否内联函数的好的启发式方法是什么?显然代码大小应该很重要,但是当(比如说)gcc 或 icc 确定是否内联函数调用时,是否还有其他通常使用的因素?该领域是否有任何重要的学术工作?
4 回答
- 除了内存大小和缓存问题之外,另一个考虑因素是寄存器压力。从编译器的角度来看,“内联过程中添加的变量可能会消耗额外的寄存器,并且在寄存器压力已经很高的区域中,这可能会强制溢出,从而导致额外的 RAM 访问。”
具有 JIT 编译器和运行时类加载的语言还有其他权衡,因为虚拟方法不是静态已知的,但 JIT 可以收集运行时分析信息,例如方法调用频率:
即时编译器 (Java)中优化的设计、实现和评估讨论了静态方法和动态加载类的方法内联及其对性能的改进。
Practicing JUDO: Java Under Dynamic Optimizations声称他们的“内联策略是基于代码大小和分析信息。如果一个方法入口的执行频率低于某个阈值,则该方法不会被内联,因为它被认为是冷的方法。为避免代码爆炸,我们不会内联字节码大小超过 25 字节的方法。... 。” 尽管它们具有运行时分析信息(方法调用频率),但它们仍然小心避免内联大型函数或函数链以防止膨胀。
在 Google Scholar 上搜索会发现许多论文,例如
在 Google Books 上搜索会发现相当多的书籍,其中包含有关各种上下文中函数内联的论文或章节。
《编译器设计手册:优化和机器代码生成》有一章是关于编译器设计中的统计和机器学习技术的,其中包含设置各种参数、分析结果的启发式方法。本章参考了 Vaswani 等人的论文Microarchitecture Sensitive Empirical Models for Compiler Optimizations,他们提出“使用经验建模技术为编译器优化构建微架构敏感模型”。
(其他一些书从程序员的角度讲inling,比如《C++ for Game Programmers》,里面讲了过于频繁地内联函数的危险以及内联和宏的区别。编译器如果能确定,往往会忽略程序员的内联请求。他们会弊大于利;这可以用宏作为最后的手段来覆盖。)
函数调用意味着一些额外的代码(函数序言,设置新堆栈帧的位置,以及函数尾声,它被清理的位置)。如果你的编译器发现函数代码与序言和尾声相比很小,它可以决定不值得进行实际调用,并将内联函数。
我看到调用函数而不是内联它的唯一好处是与大小相关。我猜想内联一个函数然后展开一个循环可能会导致大小显着增加。
据我所知,函数大小是编译器用来确定内联的唯一因素。但是,如果您进行配置文件引导优化(PGO),我相信编译器能够使用其他变量,例如调用次数/调用设置时间。
在 .NET 中主要基于大小。以编译字节为单位测量父函数和子函数的大小。然后测量组合函数的大小。如果组合函数较小,则内联是一个好主意。
这样做的原因是可以将尽可能多的代码推送到 CPU 的缓存中。缓存未命中比现代 CPU 中的函数调用要昂贵得多。