返回 2026-06-04
⚙️ 工程

内联启发式算法概述A survey of inlining heuristics

bernsteinbear.com·2026-06-03

编译器(尤其是方法级的即时编译器 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 个不同的方法调用:

  • Point.new
  • Point#initialize
  • Point.new
  • Point#initialize
  • Point#distance
  • Float#**
  • Float#**
  • Math.sqrt
  • (严格来说会更多,但 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 吧。

    启发式算法

    剧透一下:总而言之,人们倾向于关注以下几点:

  • 调用目标的剖析数据(Profiles)
  • 调用者的累积大小(随着被调用者被内联而增加)
  • 被调用者的大小
  • 内联深度
  • 在特定深度的内联调用数量
  • 是否存在递归
  • 被调用者与调用者的调用次数比(如果被调用者的调用次数少于对调用者调用次数的 K%,则不内联该被调用者)
  • 被调用者的栈使用量
  • 被调用者中的多态性
  • 编译器处于何种模式(基线模式 vs 更激进的模式)
  • 被调用者是否看起来总是抛出异常
  • 并且还有各种有趣的方式来传入剖析数据。

    最后,一些较新的论文做了一些疯狂的事情:

  • 训练神经网络来做出内联决策
  • 让内联驱动整个优化流水线,将其视为对调用图进行广度优先搜索(BFS)遍历时的一种搜索启发式算法
  • 使用 AOT 收集的信息来辅助 JIT 启发式算法
  • 内联中需要考虑的另一件事是如何收集和解读剖析数据。

    调用上下文与剖析数据:另一个更困难的部分

    当你编译一个函数时,你倾向于根据它历史上接收到的输入对其进行特化。对于单态输入,你可能会设置一个守卫条件检查类型是否仍然相同,如果不同则跳转到解释器。对于多态输入,你可能会检查前 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 引擎,多年来它采用了许多执行方式。我们将研究其中的三种,因为它们在历史上都曾占有一席之地或仍占有一席之地:

  • Hydrogen 是第一个真正的 SSA IR,由于我曾参与过 Cinder 的开发,现在又在做 ZJIT,所以它让我感到非常熟悉。不过它现在已经被废弃了。
  • Turbofan 是它的替代品,全面采用了 Sea of Nodes 架构。从大局来看,它是一个相当快的编译器,但它也毫不避讳地进行一些开销昂贵的重写操作。最近它被从 Sea of Nodes 重写为更传统的 CFG 模式,并取名为 Turboshaft。
  • Maglev 旨在与 Turbofan 并存,为了缩短编译时间,它倾向于更积极地进行推测,并减少增量重写3。
  • 它们在流水线中进行内联的时间也各不相同,这使得尝试理解这些不同的代码库变得非常有趣。

    V8 Hydrogen

    内联发生在 Hydrogen 图构建期间

  • https://github.com/tekknolagi/v8/blob/a969ab67f8e1e7475d9b26468225c3a772890c64/src/crankshaft/hydrogen.cc#L9236
  • 不存储所有函数的字节码;需要重新解析被调用者的文本源码以进行内联

    启发式算法 https://github.com/tekknolagi/v8/blob/a969ab67f8e1e7475d9b26468225c3a772890c64/src/crankshaft/hydrogen.cc#L7807

  • 一些关于原生上下文(native context)的内容
  • 根据可配置的限制检查被调用者的 AST 大小
  • 根据可配置的限制检查内联深度
  • 不内联递归函数
  • 根据可配置的限制检查当前的累积方法大小(通过 AST 节点数进行追踪)
  • 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

  • 寻找候选者 https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/compiler/js-inlining-heuristic.cc#L134
  • 是否可内联 https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/compiler/js-inlining-heuristic.cc#L75
  • 强制内联小函数 https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/compiler/js-inlining-heuristic.cc#L309
  • 遍历已排序(按比较器)的列表 https://github.com/v8/v8/blob/036842f4841326130a40adfcff38f85a9b4cd30a/src/compiler/js-inlining-heuristic.cc#L847
  • 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 的函数,用于检查在规则上是否允许内联:

  • 跳过递归
  • https://github.com/v8/v8/blob/1a391f98cc7a9196369f2d6cab7df35ffbe92c08/src/objects/shared-function-info-inl.h#L421
  • 调用次数不足(未达到最小调用频率)
  • 字节码过大
  • bool MaglevGraphBuilder::ShouldEagerInlineCall( 似乎未被使用?/ 无效的声明?也许 src/maglev/maglev-graph-builder.cc 只是在 GitHub 搜索中不起作用

    MaybeReduceResult MaglevGraphBuilder::TryBuildCallKnownJSFunction( 同样未被使用 / 同样是无效声明

    JavaScriptCore

    JavaScriptCore 很奇特!不像其他编译器那样在它们整洁小巧的 SSA IR(静态单赋值中间表示)中进行内联,JSC 是在字节码层面4进行内联的。这是他们确保至少将一层的调用上下文引入其解释器内联缓存(inline caches)的方法,这最终会为编译器提供更好的信息。

  • 字节码内联 https://github.com/WebKit/WebKit/blob/709c3895afd71e0836f8c8be7393e44d41fab7e1/Source/JavaScriptCore/bytecode/CodeBlock.cpp#L2453
  • DFG https://github.com/WebKit/WebKit/blob/709c3895afd71e0836f8c8be7393e44d41fab7e1/Source/JavaScriptCore/dfg/DFGCapabilities.cpp#L76 https://github.com/WebKit/WebKit/blob/917854a9c245b87b333e23ed4b195505d574a333/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp#L1703 https://github.com/WebKit/WebKit/blob/917854a9c245b87b333e23ed4b195505d574a333/Source/JavaScriptCore/bytecode/CallLinkStatus.cpp#L294 https://github.com/WebKit/WebKit/blob/d919344236c47b610930636d3310f00380624d43/Source/JavaScriptCore/bytecode/InlineCallFrame.h
  • 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

  • 跳过带有异常处理器的被调用者(除非通过命令行标志显式允许)
  • 跳过同步的被调用者(除非通过命令行标志显式允许)
  • 跳过包含未链接被调用者的类
  • 跳过未初始化的类
  • 启发式规则:

  • 最大内联层级(默认为 9)
  • 最大递归内联层级(默认为 1)
  • 被调用者字节码大小(顶层最大为 35 字节码,但每增加一个内联层级会递减 10%)
  • 被调用者栈使用量(最大 10 个槽位) // 限制非递归调用栈使用量的附加条件。 if ((callee_recursive_level == 0) && (callee->max_stack() + callee->max_locals() - callee->size_of_parameters() > C1InlineStackLimit)) { INLINE_BAILOUT("callee uses too much stack"); }
  • 方法总大小上限(默认 8000 字节码)
  • 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

  • 有一些较新的论文(尤其是在 Java 领域)尝试提前进行大量分析,并将生成的信息打包到 .class 文件中。这样 JIT 就能读取这些信息,从而看到比局部上下文更多的内容。或者,如果你是一个 AOT 编译器,你大概可以做更多的全系统推理——这既是因为时间预算的原因,也是因为你能一次性看到更多的函数。↩
  • 如果你感兴趣的话可以看一看。我是偶然间发现它的。↩
  • 另见“Turbolev”,它似乎以某种方式将 Maglev(CFG)与 Turbofan(Sea of Nodes)融合在了一起……↩
  • 这可能是基于一次私人对话所产生的误解。我正在设法寻找具体的实现……↩
  • 需要完整排版与评论请前往来源站点阅读。