介绍
我叫Terry Mahaffey,在MSVC的代码生成团队工作。最近,我一直在做一些工作,我们的内联线,我想给一个简短的介绍,然后再潜入一些变化,我们将运输。
内联可能是编译器执行的最重要的优化。除了除去调用开销之外,当内联决策暴露出调用者或被调用者本身都不存在的额外优化机会时,它是最有用的。例如:
int bar(int x) { int y = 1; while(--x) { y = y * 2; } return y; } int foo() { return bar(5); }
在本例中,将bar内联到foo中是一个非常好的主意;一旦完成,编译器就能够计算整个函数,foo的最终代码生成将直接返回16。
与此示例对比:
int bar(int x) { int y = 1; while(--x) { y = y * 2; } return y; } int foo(int x) { return bar(x); } int baz(int y) { return bar(y); } int zoo(int z) { return bar(z); }
在这里不太清楚,内联酒吧到福,巴兹和动物园是一个胜利。因为传入的参数不是常量,编译器无法在编译时计算出每种情况下的最终值。内联确实避免了调用开销,但这必须与bar的主体在最终程序中至少出现4次这一事实相权衡,这会增加代码大小并损害缓存的局部性。
因此,一个好的内联程序的目标是估计调用站点内联的“好处”,并将其与“成本”进行权衡(通常以代码增长来衡量)。此外,在MSVC中,我们有一个全局预算的概念,在这个预算中,我们将停止内联,而不考虑防止代码爆炸式增长的额外好处。
在visualstudio中,我们一直致力于扩展我们内联的功能,使之既能更智能地处理我们内联的内容(能够在我们以前没有的地方实现好处),又能更积极地处理(提高内联预算,降低阈值)。我们将在接下来的博客文章中有更多的话要说。但现在,我想给MSVC中的内联程序提供一些上下文,以及它与其他编译器中的内联程序的不同之处。
我们内联预优化的代码
与其他编译器不同的是,当MSVC内联一个调用时,它实际上是通过再次读入该被调用方函数的原始未优化版本来内联预优化的代码,即使被调用方此时通常已经编译和优化过。这也意味着它在可能调用函数的每个上下文中做出(并重复)内联决策,可能导致不同的结果。例如,考虑一个函数f调用g调用h,其中h内联到g中。如果g内联到f中,则h不一定也内联到f中。一种编译器,它内联后优化代码(这里是一个已经内联了h的g版本),隐式地“重放”为被调用方所做的内联决策。
我们认为这是一种优势,因为在每个上下文中重放相同的内联决策未必是最好的。但是,这会带来很大的编译时间开销,因为经常会做出相同的内联决策,并且会反复优化相同的代码。我们目前正在探索一个中间选项,在这里我们可以重放一些明显的后优化版本的函数。
我们先在线
内联是编译器完成的第一个优化。因此,编译器不仅内联调用者的预优化版本,还内联到调用者的预优化版本中。一个结果是它目前没有意识到存在一些明显的优化机会。回顾第一个示例,编译器在实现bar应该内联到foo方面做得很好,但是如果foo被改成这样:
int bar(int x) { int y = 1; while(--x) { y = y * 2; } return y; } int foo() { int x = 5; return bar(x+1); }
MSVC内联程序当前将“x+1”视为一个非常量参数,并且不会基于bar中的参数用法在内联启发式中应用额外的值。
另一个结果是,可以通过常量传播转换为直接调用的间接调用和虚拟调用还没有优化到这样做,所以我们不通过它们进行内联。您经常会看到对一个小函数的间接调用被转换为直接调用,并以最终二进制文件的形式发出,这让程序员不禁要问,为什么编译器错过了这么好的内联机会。这是订购问题;有时,优化器会在内联程序已经运行之后执行内联程序所需的优化。
这些也是我们希望在不久的将来通过在内联之前或期间执行一组有限的优化来解决的问题。
关于我们的实施
MSVC内联程序在较高级别上如下所示:
- 确定所有内联候选对象(第一组合法性检查)
- 对于每个候选人,
- 阅读候选人的身体,进行第二轮合法性检查
- 运行一系列内联启发式算法
- 如果它看起来像一个很好的内联候选对象,则递归地内联到候选对象中
- 进行最后一系列合法性检查
首先,请注意它是一个“深度优先”的内联线。朝着广度优先的方向发展是未来需要探索的一个领域。每种方法都有优点和缺点。
这些合法性检查和启发式是我们迭代的一组函数指针表。如果任何合法性检查失败,将中止该候选对象的内联。如果任何启发式检查成功,内联将向前移动。这三个合法性步骤首先基于我们在读入潜在的内联线之前对它的了解,其次是在它读入内联线之后,最后是在我们递归地扩展到被调用方之后。
合法性检查倾向于说明内联线中的限制,通常是从未实现的角落案例。像是具有复杂用户类型的参数,通过值传递,在二进制文件的不同用户定义部分内联,用try块内联函数,用setjmp内联函数,内联深度检查,我们对内联深度有严格限制,等等。
启发法并非都是平等的。有一个启发式,特别是所谓的“调用图决策”,这是我认为的“真正的”内联决策者。在这里,实现了上述关于常数参数的所有收益估计代码。调用图决策取决于自底向上的编译顺序,因为在被调用方的编译过程中(例如其参数的使用)会收集有关被调用方的某些信息,然后在内联过程中使用这些信息。还有其他一些简单的启发式算法,比如inlinee是forceinline函数,一个非常小的函数,以及一个“简单决策”的启发式算法,用于不能做出调用图决策的情况。
这个框架灵活易懂。添加一个新的合法性检查或启发式方法就像在表中添加一个条目一样简单。概要文件引导优化(profileguideoptimization,简称PGO)利用它自己的基于概要文件数据的内联决策引擎,它通过在表中有自己的条目来实现这一点。类似地,对于插入指令的构建,PGO倾向于不使用内联来帮助收集最精确的计数集。PGO通过一个简单的合法性检查来关闭插入指令的构建的内联,这个检查总是说“不”。
如果您想看到它的运行,请使用/d2inlinestats开关运行您的构建。这将打印出一个表,其中列出哪些合法性检查失败以及失败的频率,以及哪些启发式方法正在驱动成功的内联实例。
结论
我希望你觉得这个有用。在接下来的几个月里,我计划再写几篇博文,给大家一些建议,告诉大家如何更开放引擎盖,让大家更清楚地了解我们的内联器到底发生了什么,同时也谈谈我们在开发中为解决一些问题而提供的特性。如果有任何内联主题你想看到解决,请留言在下面的评论!
我们希望你能 下载Visual Studio 2019 试试看。一如既往,我们欢迎您的反馈。我们可以通过下面的评论或电子邮件联系我们( visualcpp@microsoft.com ). 如果您在使用visualstudio或MSVC时遇到问题,或者有什么建议,请告诉我们 帮助>发送反馈> 报告问题 /提供建议 在产品中,或通过 开发者社区 . 你也可以在Twitter上找到我们( @视觉 )和Facebook(msftvisualcpp)。