内联启发式算法概述A survey of inlining heuristics
编译器(尤其是方法级的即时编译器 JIT)通常以单个函数作为操作单元,因为方法通常体积较小。在 Ruby 等大量依赖方法分派的动态语言中,内联的决策对于性能优化至关重要。文章详细调查了编译器在处理代码内联时所采用的各种启发式算法。合理的内联启发式规则能够显著影响动态语言运行时的整体执行效率。
Max Bernstein
编译器,尤其是方法级即时(JIT)编译器,一次只处理一个函数。这是一个天然的代码单元大小,特别是对于动态语言的 JIT 而言:在特定的时刻,关于一个正在运行且不断变化的系统的其他部分,你还能收集到多少信息呢?
我没有任何数据来支持这一点——也许我该去收集一些——但平均而言,方法是很小的。特别是在像 Ruby 这样将方法派发用于一切操作的语言中,甚至连实例变量(属性、字段……)的查找也是如此,它们不仅小,而且随处可见。
这让编译器感到很头疼。如果我们继续对它们进行拟人化的话,编译器喜欢拥有更多的上下文信息以便更好地进行优化。来看看下面这个看似有些滑稽、但实际上却能代表惊人比例的真实代码的例子:
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def distance(other)
Math.sqrt((@x - other.x)**2 + (@y - other.y)**2)
end
end
def distance_from_origin(x, y)
Point.new(x, y).distance(Point.new(0, 0))
end目前,在 distance_from_origin 方法中,我数了一下有 8 个不同的方法调用:
(严格来说会更多,但 ivar 查找(包括 attr_reader!)、加法和减法通常都经过了特化处理,不会压入栈帧,即使在解释器中也是如此。)
此外,至少还有两次堆内存分配:每个 Point 实例各一次。
最后,还有大量针对 Point 实例的来回内存访问。
这一切太令人沮丧了!一个本该是简单的数学运算,现在却充斥着大量杂七杂八的操作。Point 显然不是一个零成本抽象。
即使我们拥有一大堆诸如加载-存储消除或逃逸分析等其他优化手段,它们也发挥不了多大作用:几乎所有东西都逃逸了并且带有副作用。也就是说,除非我们进行内联。内联是一根能够撬动大量其他优化阶段生效的杠杆。
内联:“简单”的部分
几年前,我写过一篇关于 Cinder 内联器设计与实现的文章(FB 链接,个人博客链接)。我当时写的可以说是最简单的部分,也就是将被调用者的函数体复制到调用者中。我花了至少一周的时间才让它正常工作。如果把 JIT 其余部分的配套集成工作也算进去,可能要花上几个月。而在今年 2 月的一场小型黑客松上,我看着我的同事 k0kubun 在短短 30 分钟内,就在 ZJIT 中写出了内联器这部分功能的原型。
当虚拟机(VM)的几乎每一个部分都能从客座语言中被观察到时,还有更多的工作要做:Python 和 Ruby 都允许从用户代码中检查局部变量、调用栈等的状态。采样分析器也需要保留一定量的调用痕迹才能用来检查堆栈。因此,我们还需要引入更多的机制,来伪装成被调用函数没有被内联一样。我在那篇关于 Cinder 的博客文章中稍微讨论过这一点。
即便如此,所有这些大概也能在几个月内设计并串联完成。但在那之后,你就会发现自己要在接下来的 10 年里不断对内联器进行调优。这才是难得多的事情。
何时内联:更困难的部分
使得内联变得困难的原因,特别是在方法级 JIT 中,在于你试图让整个(动态!)系统变得更快,但你只能通过显微镜进行观察,并且只能进行局部推理1。虽然强度削弱、内联缓存和值编号等其他优化对生成的代码来说百利而无一害,但内联却可能产生负面影响。它可能也是人们加入的第一个具有非局部影响的优化。
如果内联不当,你的代码体积可能会急剧膨胀。这可能会导致 CPU 缓存颠簸。这确实令人头疼,但再厉害的高手也难免会遇到。
而且,如果内联不当,可能会阻碍其他有益的优化:如果在内联方法 A 之后达到了某种大小限制,你可能永远无法内联 B,而 B 恰恰是解锁你正试图优化的那个方法性能的关键。
最后,内联可能会拖慢编译时间。在延迟至关重要的情况下(想想:交互式客户端 JavaScript),即使长期吞吐量有所提高,向其中加入大量代码也可能导致明显的卡顿。一如既往,带内编译是一种权衡,因为你花在编译上的时间,就没有在执行代码。
你必须编写编译器来推演所有这些事情。因此你需要启发式算法。例如,这是 Michael Pollan 的内联启发式规则:
内联方法。主要是小方法。数量不要太多。
我对一堆编译器(主要是 JIT 编译器)做了一项调查,看看它们的内联启发式算法是什么样的。我还阅读(略读)了一些论文,看看那些研究者是怎么说的。我想知道他们是否意见一致。
这篇文章酝酿了很长时间。大约五年前我就开始写了,但后来当我从 Facebook 离职时,我不小心把为 Cinder 内联器所做的所有内联研究都留在了那里。所以后来我有一阵子只是漫无目的地思考,直到今年才重新开始做这件事。不管怎样,这就当是我的 wonderwall 吧。
启发式算法
剧透一下:总而言之,人们倾向于关注以下几点:
并且还有各种有趣的方式来传入剖析数据。
最后,一些较新的论文做了一些疯狂的事情:
内联中需要考虑的另一件事是如何收集和解读剖析数据。
调用上下文与剖析数据:另一个更困难的部分
当你编译一个函数时,你倾向于根据它历史上接收到的输入对其进行特化。对于单态输入,你可能会设置一个守卫条件检查类型是否仍然相同,如果不同则跳转到解释器。对于多态输入,你可能会检查前 K 个(约 4 个)常见情况,如果不匹配则跳转到解释器。这很好。
但有时你可能正在编译一个多态方法 bar,而该方法在其调用者 foo 中实际上是单态的。也就是说,foo 可能只向 bar 传递一种类型的输入,但其他调用者却传递各种各样的东西。这里有一个有点傻的例子来说明我的意思:
class HashWithIndifferentAccess
def initialize
@hash = {}
end
# Allow reading from the Hash with either a String or a Symbol
def [](key) = @hash[key.to_sym]
# ...
end
# some method...
some_hash = HashWithIndifferentAccess.new
# ...
some_hash["abc"]
# some other method...
another_hash = HashWithIndifferentAccess.new
# ...
another_hash[:xyz]开个玩笑,其实一点也不傻。这是 Rails 中非常常见的一种模式。它使得 HashWithIndifferentAccess#[] 中的 key 变得多态,即使对于它的许多调用者来说,它很可能是单态的(甚至是一个常量)。
为了将这些信息传递给编译器,你必须弄清楚这种调用上下文关系。有几种常见的方法可以做到这一点。
拆分(Splitting)
以 YJIT 为例,尽管它不做内联,但会根据传入参数的类型对方法进行拆分。这意味着它会克隆已编译的代码,为每个上下文生成一个新版本。这并不能提供调用上下文(“A 调用 B”),但提供了类型上下文(“B 在传入整数时被调用,B’ 在传入字符串时被调用”)。
编译器可以在解释器或基线层中进行基于类型的拆分。
Profile 拆分
如果你不想复制代码,可以选择复制 Profile。你可以使用类型上下文(如上所述)或调用上下文来实现这一点。例如,SpiderMonkey 采用“试验性内联”机制,允许调用者向下传递一块内存,供潜在的内联候选被调用者记录其内联缓存(IC)。此时,而不是让每个函数持有各自的 ICScript,而是由调用者为该潜在的内联调用点分配一个专属的 ICScript。这就为每个被调用函数提供了(至少?)一层调用上下文。
随后,当把被调用者内联到调用者中时,我们不会受到其他调用者的类型信息对 IR 构建器(或其他读取 Profile 的组件)的污染。
字节码内联
JavaScriptCore 通过将字节码内联到其他字节码中来处理这个问题。这是一个相当复杂的转换,但它甚至(!)让解释器也能访问调用上下文。当分层升级到编译器时,所有的内联决策都已经完成了。
带计数器的早期层
HotSpot 通过多个分层来处理这个问题。解释器升级到客户端编译器 C1。C1 会在编译后的代码中收集分支和调用目标的 Profile。C1 最终可能会根据这些新信息进行重新编译。C1 最终也可能会升级到 C2,而 C2 会直接沿用 C1 的内联决策。通过这种方式,我们借助内联在 Profile 中获得了调用上下文。
内联、分析并祈祷
你能做的最后一件事,就是完全相信优化器中的类型推断和分支折叠。你可以在构建 IR 时内联并在被调用者中进行多态特化,然后寄希望于分支修剪能将内联后的被调用者单态化。这其实有点浪费资源,因为多态代码的构建算是“白费力气”了,但这方法说不定也行得通?
好了,接下来是收集的笔记和一些粗浅的见解。以下是对一系列 JIT 编译器及其如何处理内联启发式方法的综述。
致谢
但在进入正题之前,感谢 Iain Ireland、CF Bolz-Tereick 和 Ian Rogers 对这篇博文的反馈!
综述:杂项记录
接下来的部分主要是类似于 Phil Zucker 风格的“杂项记录”。
我们将从 Cinder 开始,因为在我编写 Cinder 的内联器时,我只添加了最简单的启发式规则,主要都是些“不要内联”的信号。随着时间的推移,在我离开之后,大家对它进行了更多的调优。
Cinder
内联器从调用者的 CFG(控制流图)开始,对其进行遍历以寻找合适的内联候选者。内联候选者仅针对已知的调用目标——在 Cinder 中,仅针对单态调用目标——并且需要通过一些检查。被调用者仅通过其函数对象(包含字节码)来识别。在我们决定内联之前,被调用者是没有可用的 IR 的。
大多数“无法处理”的检查都与参数处理有关。Python 的调用约定相当复杂,因此如果调用者/被调用者没有在参数传递方式上达成一致,内联器就不会费心去自己解决这个问题。那是编译器其他部分的职责。canInline 函数中的许多内容都可以被视为“待办事项(TODO)”。
bool canInline(Function& caller, AbstractCall* call_instr) {
// ...
BorrowedRef<PyFunctionObject> func = call_instr->func;
auto fail = [&](InlineFailureType failure_type) {
dlogAndCollectFailureStats(caller, call_instr, failure_type);
return false;
};
if (func->func_kwdefaults != nullptr) {
return fail(InlineFailureType::kHasKwdefaults);
}
BorrowedRef<PyCodeObject> code{func->func_code};
JIT_CHECK(PyCode_Check(code), "Expected PyCodeObject");
if (code->co_kwonlyargcount > 0) {
return fail(InlineFailureType::kHasKwOnlyArgs);
}
// ...
}失败会被记录下来以便分析。如果 Cinder 团队确定存在某些他们需要处理的频繁情况,他们会从日志中发现问题。
内联器在对 CFG 进行的一次遍历中收集所有候选的调用指令。它从选项结构体中加载可配置的“成本限制”。然后它对内联候选向量进行一次遍历,不断进行内联,直到(可能)达到成本限制。
// ...
size_t cost_limit = getConfig().inliner_cost_limit;
size_t cost = codeCost(irfunc.code);
// Inline as many calls as possible, starting from the top of the function and
// working down.
for (auto& call : to_inline) {
BorrowedRef<PyCodeObject> call_code{call.func->func_code};
size_t new_cost = cost + codeCost(call_code);
if (new_cost > cost_limit) {
LOG_INLINER(
"Inliner reached cost limit of {} when trying to inline {} into {}, "
"inlining stopping early",
new_cost,
funcFullname(call.func),
irfunc.fullname);
break;
}
cost = new_cost;
inlineFunctionCall(irfunc, &call);
// We need to reflow types after every inline to propagate new type
// information from the callee.
reflowTypes(irfunc);
}
// ...在内联这些调用之后,它会做一些图维护工作,但也仅此而已。
尽管非常简单,这种方法却获得了惊人的实用性:它内联常量(相当多的方法看起来像 `def foo(): return 5`)和小型方法,并且(至少据我所知)能缩小编译后的代码体积。而这一切只需极少的编译时间开销。
目前还有另一种“独立”的 Python JIT,也就是 PyPy。所以我们也应该看看它。
PyPy
PyPy 中有两个内联器。一个位于 RPython 到 C 的转换流水线中,它的表现更像是一个提前编译器2。另一个是追踪型 JIT 部分,它有自己的优化器和启发式算法。我们将关注后者。
我和 CF Bolz-Tereick 谈论过这个内联器,他们的评价是 PyPy 的内联启发式算法就是“同意”。有一些例外情况,比如不内联递归函数或带有循环的函数。但追踪的基本思想包括穿透调用指令进行追踪,这自然意味着你正在进行“内联”。
PyPy 还做了一件很巧妙的事情,他们将栈帧压入视为普通的内存分配。栈帧压入、栈帧读取和栈帧写入像普通的对象内存流量一样被写入追踪记录中,并且可以像其他的字段读写一样被优化掉。这意味着他们可以“直接”使用 DCE 来消除栈帧的压入和弹出,而 Cinder 则使用了一些复杂的机制来实现这一点(这是我的锅)。
TODO 在此处补充更多细节
V8
V8 是一个 JS 引擎,多年来它采用了许多执行方式。我们将研究其中的三种,因为它们在历史上都曾占有一席之地或仍占有一席之地:
它们在流水线中进行内联的时间也各不相同,这使得尝试理解这些不同的代码库变得非常有趣。
V8 Hydrogen
内联发生在 Hydrogen 图构建期间
不存储所有函数的字节码;需要重新解析被调用者的文本源码以进行内联
启发式算法 https://github.com/tekknolagi/v8/blob/a969ab67f8e1e7475d9b26468225c3a772890c64/src/crankshaft/hydrogen.cc#L7807
V8 TurboFan
https://docs.google.com/document/d/1VoYBhpDhJC4VlqMXCKvae-8IGuheBGxy32EOgC2LnT8/edit
https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/compiler/js-inlining-heuristic.h#L14
V8 Maglev
在优化时,将调用指令添加到内联候选列表中:https://github.com/v8/v8/blob/1a391f98cc7a9196369f2d6cab7df35ffbe92c08/src/maglev/maglev-graph-optimizer.cc#L1271
ProcessResult MaglevGraphOptimizer::VisitCall(Call* node,
const ProcessingState& state) {
// ...
int bytecode_length = shared.GetBytecodeArray(broker()).length();
float score =
(call_frequency / bytecode_length) * (loop_depth_ > 0 ? 1.5 : 1.0);
bool is_small_function =
bytecode_length <
reducer_.graph()->compilation_info()->flags().max_eager_inlined_bytecode;
// ...
MaglevCallSiteInfo* call_site = reducer_.zone()->New<MaglevCallSiteInfo>(
MaglevCallerDetails{
...
is_small_function, call_frequency,
...
},
score, bytecode_length);
reducer_.PushInlineCandidate(call_site);
// ...
}https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/maglev/maglev-inlining.h#L36
与 Cinder 等不同,Maglev 似乎对于什么可以被内联到哪里没有太多限制,因此它的“是否可内联”信号主要取决于预算。实际上包含两种预算:小预算和普通预算。
bool MaglevInliner::CanInlineCall() {
// We stop inlining entirely if the small budget is exhausted.
// Inlining decisions after that become bad if we stop inlining small
// functions, but keep inlining large ones.
return !graph_->inlineable_calls().empty() &&
(graph_->total_inlined_bytecode_size() <
max_inlined_bytecode_size_cumulative() ||
graph_->total_inlined_bytecode_size_small() <
max_inlined_bytecode_size_small_total());
}然后它的内联循环会以贪心方式遍历待内联队列,检查候选者的大小。
bool MaglevInliner::InlineCallSites() {
DCHECK(CanInlineCall());
while (!graph_->inlineable_calls().empty()) {
// pop from inlineable_calls
MaglevCallSiteInfo* call_site = ChooseNextCallSite();
bool is_small_with_heapnum_input_outputs =
IsSmallWithHeapNumberInputsOutputs(call_site);
if (graph_->total_inlined_bytecode_size() >
max_inlined_bytecode_size_cumulative()) {
// We ran out of budget. Checking if this is a small-ish function that we
// can still inline.
if (graph_->total_inlined_bytecode_size_small() >
max_inlined_bytecode_size_small_total()) {
graph_->compilation_info()->set_could_not_inline_all_candidates();
break;
}
if (!is_small_with_heapnum_input_outputs) {
graph_->compilation_info()->set_could_not_inline_all_candidates();
// Not that we don't break just rather just continue: next candidates
// might be inlineable.
continue;
}
}
InliningResult result =
BuildInlineFunction(call_site, is_small_with_heapnum_input_outputs);
// ...
}
return true;
}它将这个循环(清空队列)与优化器(填充队列)交替运行。
bool MaglevInliner::Run() {
if (graph_->inlineable_calls().empty()) return true;
while (CanInlineCall()) {
if (!InlineCallSites()) return false;
RunOptimizer();
}
// ...
}然而令人困惑的是,优化器还会调用另一个名为 CanInlineCall 的函数,用于检查在规则上是否允许内联:
bool MaglevGraphBuilder::ShouldEagerInlineCall( 似乎未被使用?/ 无效的声明?也许 src/maglev/maglev-graph-builder.cc 只是在 GitHub 搜索中不起作用
MaybeReduceResult MaglevGraphBuilder::TryBuildCallKnownJSFunction( 同样未被使用 / 同样是无效声明
JavaScriptCore
JavaScriptCore 很奇特!不像其他编译器那样在它们整洁小巧的 SSA IR(静态单赋值中间表示)中进行内联,JSC 是在字节码层面4进行内联的。这是他们确保至少将一层的调用上下文引入其解释器内联缓存(inline caches)的方法,这最终会为编译器提供更好的信息。
JSC 仅基于字节码性能分析(profile)信息进行内联,并且只内联字节码??
TODO 寻找关于字节码内联更好的资料
SpiderMonkey
SpiderMonkey 有另一种无需进行字节码内联即可获取调用上下文的方法:它们将调用上下文添加到了内联缓存中。方法可以将一个 ICScript 向下传递给它的被调用者(callees),被调用者会在其中写入它的内联缓存信息。这样一来,在编译时,被调用者就更有可能被单态化(monomorphized)。
Wasm
https://github.com/mozilla-firefox/firefox/blob/438a3ce10eb77fb50d968463b7741117aec5bb4a/js/src/wasm/WasmHeuristics.h#L213
SpiderMonkey ICScript
Wasmtime 和 Cranelift
https://fitzgen.com/2025/11/19/inliner.html
HotSpot
计划:在解释器中运行;分层升级至 C1;分析调用目标;在 C1 中进行内联;分析分支执行次数;分层升级至 C2,C2 会在字节码解析器中复制 C1 的内联决策
HotSpot C2
https://github.com/openjdk/jdk/blob/a05d5d2514c835f2bfeaf7a8c7df0ac241f0177f/src/hotspot/share/opto/bytecodeInfo.cpp#L116
https://github.com/openjdk/jdk/blob/497dca2549a9829530670576115bf4b8fab386b3/src/hotspot/share/opto/bytecodeInfo.cpp#L197
https://github.com/openjdk/jdk/blob/497dca2549a9829530670576115bf4b8fab386b3/src/hotspot/share/opto/parse.hpp#L42
https://github.com/openjdk/jdk/blob/497dca2549a9829530670576115bf4b8fab386b3/src/hotspot/share/opto/doCall.cpp#L185
不能太小
向上遍历调用栈以确定要编译的内容
处理正确的内联对象:例如 def foo(a) = a.each {|x| x },我们希望编译 foo,内联 each,再内联代码块,而不是单独编译代码块(通常情况下)
HotSpot C1
https://bernsteinbear.com/assets/img/design-hotspot-client-compiler.pdf
https://github.com/openjdk/jdk/blob/d854a04231a437a6af36ae65780961f40f336343/src/hotspot/share/c1/c1_GraphBuilder.cpp#L755
https://github.com/openjdk/jdk/blob/d854a04231a437a6af36ae65780961f40f336343/src/hotspot/share/c1/c1_GraphBuilder.cpp#L3854
启发式规则:
TruffleRuby
TruffleRuby 使用加权的编译队列
Graal https://ieeexplore.ieee.org/document/8661171
.NET
https://github.com/dotnet/runtime/blob/2d638dc1179164a08d9387cbe6354fe2b7e4d823/docs/design/coreclr/jit/inlining-plans.md
https://github.com/dotnet/runtime/blob/0b3f3ab1ecf4de06459e5f0e2b7cb3baf70ef981/src/coreclr/jit/inline.def#L94
https://github.com/dotnet/runtime/blob/0b3f3ab1ecf4de06459e5f0e2b7cb3baf70ef981/src/coreclr/jit/inlinepolicy.cpp
https://github.com/dotnet/runtime/blob/0b3f3ab1ecf4de06459e5f0e2b7cb3baf70ef981/docs/design/coreclr/jit/inline-size-estimates.md?plain=1#L5 https://github.com/dotnet/runtime/blob/0b3f3ab1ecf4de06459e5f0e2b7cb3baf70ef981/src/coreclr/jit/fginline.cpp
https://github.com/dotnet/runtime/issues/10303
https://github.com/AndyAyersMS/PerformanceExplorer/blob/master/notes/notes-aug-2016.md
Dart
https://github.com/dart-lang/sdk/blob/391212f3da8cc0790fc532d367549042216bd5ca/runtime/vm/compiler/backend/inliner.cc#L49
https://github.com/dart-lang/sdk/blob/391212f3da8cc0790fc532d367549042216bd5ca/runtime/vm/compiler/backend/inliner.cc#L1023
https://web.archive.org/web/20170830093403id_/https://link.springer.com/content/pdf/10.1007/978-3-540-78791-4_5.pdf
DEFINE_FLAG(int,
deoptimization_counter_inlining_threshold,
12,
"How many times we allow deoptimization before we stop inlining.");
DEFINE_FLAG(bool, trace_inlining, false, "Trace inlining");
DEFINE_FLAG(charp, inlining_filter, nullptr, "Inline only in named function");
// Flags for inlining heuristics.
DEFINE_FLAG(int,
inline_getters_setters_smaller_than,
10,
"Always inline getters and setters that have fewer instructions");
DEFINE_FLAG(int,
inlining_depth_threshold,
6,
"Inline function calls up to threshold nesting depth");
DEFINE_FLAG(
int,
inlining_size_threshold,
25,
"Always inline functions that have threshold or fewer instructions");
DEFINE_FLAG(int,
inlining_callee_call_sites_threshold,
1,
"Always inline functions containing threshold or fewer calls.");
DEFINE_FLAG(int,
inlining_callee_size_threshold,
160,
"Do not inline callees larger than threshold");
DEFINE_FLAG(int,
inlining_small_leaf_size_threshold,
50,
"Do not inline leaf callees larger than threshold");
DEFINE_FLAG(int,
inlining_caller_size_threshold,
50000,
"Stop inlining once caller reaches the threshold.");
DEFINE_FLAG(int,
inlining_hotness,
10,
"Inline only hotter calls, in percents (0 .. 100); "
"default 10%: calls above-equal 10% of max-count are inlined.");
DEFINE_FLAG(int,
inlining_recursion_depth_threshold,
1,
"Inline recursive function calls up to threshold recursion depth.");
DEFINE_FLAG(int,
max_inlined_per_depth,
500,
"Max. number of inlined calls per depth");一种自适应的内联替换策略 (PDF)
// Inlining heuristics based on Cooper et al. 2008.
InliningDecision ShouldWeInline(const Function& callee,
intptr_t instr_count,
intptr_t call_site_count) {
// Pragma or size heuristics.
if (inliner_->AlwaysInline(callee)) {
return InliningDecision::Yes("AlwaysInline");
} else if (inlined_size_ > FLAG_inlining_caller_size_threshold) {
// Prevent caller methods becoming humongous and thus slow to compile.
return InliningDecision::No("--inlining-caller-size-threshold");
} else if (instr_count > FLAG_inlining_callee_size_threshold) {
// Prevent inlining of callee methods that exceed certain size.
return InliningDecision::No("--inlining-callee-size-threshold");
}
// Inlining depth.
const int callee_inlining_depth = callee.inlining_depth();
if (callee_inlining_depth > 0 &&
((callee_inlining_depth + inlining_depth_) >
FLAG_inlining_depth_threshold)) {
return InliningDecision::No("--inlining-depth-threshold");
}
// Situation instr_count == 0 denotes no counts have been computed yet.
// In that case, we say ok to the early heuristic and come back with the
// late heuristic.
if (instr_count == 0) {
return InliningDecision::Yes("need to count first");
} else if (instr_count <= FLAG_inlining_size_threshold) {
return InliningDecision::Yes("--inlining-size-threshold");
} else if (call_site_count <= FLAG_inlining_callee_call_sites_threshold) {
return InliningDecision::Yes("--inlining-callee-call-sites-threshold");
}
return InliningDecision::No("default");
}HHVM
基于 tracelet
https://github.com/facebook/hhvm/blob/eeba7ad1ffa372a9b8cc9d1ec7f5295d45627009/hphp/runtime/vm/jit/inlining-decider.h#L89
// Refuse if the cost exceeds our thresholds.
// We measure the cost of inlining each callstack and stop when it exceeds a
// certain threshold. (Note that we do not measure the total cost of all the
// inlined calls for a given caller---just the cost of each nested stack.)
cost = costOfInlining(callerSk, callee, regionAndUnit, annotationsPtr);
if (cost <= Cfg::HHIR::AlwaysInlineVasmCostLimit) {
return accept(folly::sformat("cost={} within always-inline limit", cost));
}
if (region.instrSize() > irgs.budgetBCInstrs) {
return refuse(folly::sformat(
"exhausted bytecode budget: budgetBCInstrs={}, regionSize={}",
irgs.budgetBCInstrs, region.instrSize()));
}
auto maxTotalCost = adjustedMaxVasmCost(irgs, region, inlineDepth(irgs));
int maxCost = maxTotalCost;
if (Cfg::HHIR::InliningUseStackedCost) {
maxCost -= irgs.inlineState.cost;
}
const auto baseProfCount = s_baseProfCount.load();
const auto callerProfCount = irgen::curProfCount(irgs);
const auto calleeProfCount = irgen::calleeProfCount(irgs, region);
if (cost > maxCost) {
auto const depth = inlineDepth(irgs);
return refuse(folly::sformat(
"too expensive: cost={} : maxCost={} : "
"baseProfCount={} : callerProfCount={} : calleeProfCount={} : depth={}",
cost, maxCost, baseProfCount, callerProfCount, calleeProfCount, depth));
}
return accept(folly::sformat("small region with return: cost={} : "
"maxTotalCost={} : maxCost={} : baseProfCount={}"
" : callerProfCount={} : calleeProfCount={}",
cost, maxTotalCost, maxCost, baseProfCount,
callerProfCount, calleeProfCount));ART
https://github.com/LineageOS/android_art/blob/8ce603e0c68899bdfbc9cd4c50dcc65bbf777982/compiler/optimizing/inliner.h
// Instruction limit to control memory.
static constexpr size_t kMaximumNumberOfTotalInstructions = 1024;
// Maximum number of instructions for considering a method small,
// which we will always try to inline if the other non-instruction limits
// are not reached.
static constexpr size_t kMaximumNumberOfInstructionsForSmallMethod = 3;
// Limit the number of dex registers that we accumulate while inlining
// to avoid creating large amount of nested environments.
static constexpr size_t kMaximumNumberOfCumulatedDexRegisters = 32;
// Limit recursive call inlining, which do not benefit from too
// much inlining compared to code locality.
static constexpr size_t kMaximumNumberOfRecursiveCalls = 4;
// Limit recursive polymorphic call inlining to prevent code bloat, since it can quickly get out of
// hand in the presence of multiple Wrapper classes. We set this to 0 to disallow polymorphic
// recursive calls at all.
static constexpr size_t kMaximumNumberOfPolymorphicRecursiveCalls = 0;
// Controls the use of inline caches in AOT mode.
static constexpr bool kUseAOTInlineCaches = true;
// Controls the use of inlining try catches.
static constexpr bool kInlineTryCatches = true;void HInliner::UpdateInliningBudget() {
if (total_number_of_instructions_ >= kMaximumNumberOfTotalInstructions) {
// Always try to inline small methods.
inlining_budget_ = kMaximumNumberOfInstructionsForSmallMethod;
} else {
inlining_budget_ = std::max(
kMaximumNumberOfInstructionsForSmallMethod,
kMaximumNumberOfTotalInstructions - total_number_of_instructions_);
}
}bool HInliner::IsInliningEncouraged(const HInvoke* invoke_instruction,
ArtMethod* method,
const CodeItemDataAccessor& accessor) const {
if (CountRecursiveCallsOf(method) > kMaximumNumberOfRecursiveCalls) {
LOG_FAIL(stats_, MethodCompilationStat::kNotInlinedRecursiveBudget)
<< "Method "
<< method->PrettyMethod()
<< " is not inlined because it has reached its recursive call budget.";
return false;
}
size_t inline_max_code_units = codegen_->GetCompilerOptions().GetInlineMaxCodeUnits();
if (accessor.InsnsSizeInCodeUnits() > inline_max_code_units) {
LOG_FAIL(stats_, MethodCompilationStat::kNotInlinedCodeItem)
<< "Method " << method->PrettyMethod()
<< " is not inlined because its code item is too big: "
<< accessor.InsnsSizeInCodeUnits()
<< " > "
<< inline_max_code_units;
return false;
}
if (graph_->IsCompilingBaseline() &&
accessor.InsnsSizeInCodeUnits() > CompilerOptions::kBaselineInlineMaxCodeUnits) {
LOG_FAIL_NO_STAT() << "Reached baseline maximum code unit for inlining "
<< method->PrettyMethod();
outermost_graph_->SetUsefulOptimizing();
return false;
}
if (invoke_instruction->GetBlock()->GetLastInstruction()->IsThrow()) {
LOG_FAIL(stats_, MethodCompilationStat::kNotInlinedEndsWithThrow)
<< "Method " << method->PrettyMethod()
<< " is not inlined because its block ends with a throw";
return false;
}
return true;
}if (outermost_graph_->IsCompilingBaseline() &&
(current->IsInvokeVirtual() || current->IsInvokeInterface()) &&
ProfilingInfoBuilder::IsInlineCacheUseful(current->AsInvoke(), codegen_)) {
uint32_t maximum_inlining_depth_for_baseline =
InlineCache::MaxDexPcEncodingDepth(
outermost_graph_->GetArtMethod(),
codegen_->GetCompilerOptions().GetInlineMaxCodeUnits());
if (depth_ + 1 > maximum_inlining_depth_for_baseline) {
LOG_FAIL_NO_STAT() << "Reached maximum depth for inlining in baseline compilation: "
<< depth_ << " for " << callee_graph->GetArtMethod()->PrettyMethod();
outermost_graph_->SetUsefulOptimizing();
return false;
}
}JikesRVM
https://github.com/JikesRVM/JikesRVM/blob/5072f19761115d987b6ee162f49a03522d36c697/rvm/src/org/jikesrvm/compilers/opt/inlining/DefaultInlineOracle.java#L55
其他/研究
部分内联
理解与利用最优函数内联 (PDF)
机器学习
使用机器学习自动构建内联启发式算法
动态编译器中基于机器学习的优化启发式算法 (PDF)
使用内联后转换来指导内联决策 (PDF)
你无法内联这个!(PDF)
利用内联试探做出更好的内联决策
RhizomeRuby 内联
一种面向 JIT 编译器的优化驱动增量内联替换算法 (PDF)
内联启发式算法的自动调优 (PDF)
基于过程间部分逃逸分析的内联收益预测 (PDF)
虚方法的内联 (PDF)
JIT 环境下推测性方法内联的类型分析研究 (PDF)
内联的静态与基于性能剖析的启发式算法比较研究 (PDF)
Falcon JIT 中自定义收益驱动内联器的簇 (PDF)
Graal
https://github.com/oracle/graal/blob/5dde777cba22a99ebe3f19745d03ddfbc35c563c/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/phases/common/inlining/policy/GreedyInliningPolicy.java
https://github.com/oracle/graal/blob/5dde777cba22a99ebe3f19745d03ddfbc35c563c/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/phases/common/inlining/InliningPhase.java
https://github.com/oracle/graal/blob/5dde777cba22a99ebe3f19745d03ddfbc35c563c/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/phases/common/inlining/info/elem/InlineableGraph.java#L148
需要完整排版与评论请前往来源站点阅读。