OSR(On-Stack Replacement)是怎样的机制?

软件工程师、主攻高级编程语言虚拟机的设计与实现

398 👍 / 20 💬

问题描述

关于OSR,我差不多能明白,这是一种运行时替换栈帧的技术。但我在看V8的代码的时候,还是会感觉一头雾水。求指教。
我现在大概知道的是这样的:
1. Hydrogen IR中会创建OsrEntry。
2. profiler, builtins, runtime中与OnStackRelacement相关的代码。单独看好像是看明白了,但是想串成一整块,好像又与自己理解的不同。
3. 与execution还有一些关系?
希望高手解答。

On-Stack Replacement (OSR) 是一种对提升benchmark跑分非常有效的技术,而对普通的结构良好的程序…(咳咳

OSR是一种在运行时替换正在运行的函数/方法的栈帧的技术。但它是手段,不是目的——是出于某种目的需要在运行时替换栈帧。

使用OSR最常见的目的就是在一个函数/方法的执行过程中,在执行引擎的不同优化层级之间切换,可以是从低优化层级向高优化层级切换,也可以反过来。这也就隐含了一个假设——这个执行引擎有多个层级的优化,可能是

在多层优化的执行引擎中,OSR可以为两种目的服务:

OSR还有若干其它叫法。

IBM曾经在J9 JVM中把OSR叫做“dynamic loop transfer”(DLT)。这个纯属傲娇,DLT说的就是在循环中做从低优化到高优化层级的OSR。

而HotSpot VM(以及同门师弟V8)也有自己的傲娇:它把从低优化层级向高优化层级的迁移叫做OSR,而把高优化层级向低优化层级的迁移叫做“去优化”(deoptimize),也叫做“uncommon trap”。其实deoptimize也是OSR的一种情况,能理解这个就够了。

Chakra中,deoptimization的对应物叫做“bailout”,同一个东西的不同叫法而已。

正好题主问的另外一个跟V8相关的问题也跟OSR(deoptimize)有关,放个传送门:

HSimulate这条Hydrogen里的instruction到底是什么意思? - RednaxelaFX 的回答

把OSR机制推广开来,所谓generalized on-stacking replacement,其实就是trace-based compilation中会用到的一种基础技术。Trace编译后,进入trace和离开trace其实都要做OSR。

在trace-based compilation的上下文中,离开trace的OSR的某些情况会叫做“side-exit”,其实就跟上面提到的“deoptimize”是一个道理。

tl;dr:“执行引擎有多个不同优化程度的层级,一个函数正在执行的过程中可以在不同优化层级之间迁移”就是OSR的重点

知道这个之后,下面都是废话。不怕我啰嗦的话请继续看下去…

===========================================

OSR是在1980年代末1990年代初的Self VM就发展成熟的技术,在JVM上得到了广泛应用,现在各大JavaScript引擎上也普遍采用了该技术。

有好几篇Self VM的论文重点提到了OSR的思路:

要追根溯源的话请务必细读这些Self VM的论文。

V8 是 Self VM -> Strongtalk VM 血缘下的嫡系,跟 HotSpot JVM 是同门兄弟,而V8 Crankshaft中的优化JIT编译器更是从HotSpot VM的Client Compiler(C1)移植而来(美其名曰“深受影响”),V8 Crankshaft中的OSR机制与HotSpot VM的OSR有非常紧密的联系。

所以下面先用Java来举例,然后再回到JavaScript(V8)的情况。

试想一个microbenchmark,我们想测试Java的Math.sin()方法的速度,要怎么测?

入门级程序员(或者从C/C++之类的通常不动态编译的语言转到Java的程序员)会这样写:

public class BadMicrobenchmark {
  public static void main(String[] args) {
    long startTime = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
      Math.sin(i);
    }
    long endTime = System.nanoTime();
    System.out.println("duration (ns): " + (endTime - startTime));
  }
}

在主流环境中,JVM拿到手的是含有Java字节码的Class文件,并不能直接在硬件上执行,而需要JVM要么解释执行之,要么做JIT编译后执行。为了平衡启动性能与顶峰性能的需求,现代主流JVM都引入了多层编译机制,在刚开始的时候采用比较低优化的层级来执行,等某块代码热了之后再使用较高优化的层级来执行。

这大背景请参考下面俩传送门:

但是问题就来了:这些主流JVM通常是以“方法”(或者笼统的说,“函数”)为单位来JIT编译的。一个方法新JIT编译后的版本,在编译好之后,要等到下一次该方法被调用时才能用上,而无法顾及当前正在执行的方法的情况。

以上面的microbenchmark例子看,这个main()方法在整个程序的执行过程中只会被调用一次,就算JVM知道它很热而把它给JIT编译了,也没有机会等到它第二次被调用的时候跳进JIT编译的版本里。这样跑benchmark不就废了么?

于是OSR机制就来救场了!

与其编译整个方法,我们可以在发现某个方法里有循环很热的时候,选择只编译方法里的某个循环,或者是从某个循环开始的代码。编译好之后,执行引擎便在仍在执行该方法的情况下,从原本的层级跳转到这个新JIT编译好的版本的代码去。

还是以上面的代码为例,我们可以只编译循环的这部分(情况A):

    startTime = ???; // not used in this compilation, but used by deoptimization
    i = ???;
    for ( ; i < 10_000_000; i++) {
      Math.sin(i);
    }
    Runtime.deoptimize(startTime, i); // tail call: deoptimize and go back to interpreter

或者只编译从这个循环开始的部分(情况B):

    startTime = ???;
    i = ???;
    for ( ; i < 10_000_000; i++) {
      Math.sin(i);
    }
    long endTime = System.nanoTime();
    System.out.println("duration (ns): " + (endTime - startTime));
    return; // end of method, normal return

注意:我们要发现一个循环很热,肯定是已经执行了该循环很多次了,并且在触发该循环的JIT编译时该循环还没执行完。

所以我们JIT编译的并不是完整的该循环(用上例说就是 i = 0 开始),而是该循环可能已经执行了很多次之后再进入的该循环(所以上面就用 i = ??? 表示)。

于是我们就需要从原本该方法所执行的层级的栈帧中,把需要的状态找出来,然后迁移到新的优化层级的栈帧去。在上面的情况B中,我们显然需要从原本的栈帧中找出局部变量 startTime 与 i 的值,并将其迁移到新编译的版本的代码的栈帧去。

假如我们通过一个叫做“OSR buffer”的地方来从低优化层级向高优化层级传递值,那么上面情况B所编译的代码的样子就会是这样的:(伪代码)

  public static void main$osr_at_bci$21(OsrBuffer osrbuf) {
    // OSR entry
    startTime = osrbuf.startTime; // osrbuf.slots[1]
    i         = osrbuf.i;         // osrbuf.slots[3]
    // actual body
    for ( ; i < 10_000_000; i++) {
      Math.sin(i);
    }
    long endTime = System.nanoTime();
    System.out.println("duration (ns): " + (endTime - startTime));
    return; // end of method, normal return
  }

假如我们是在“解释器+JIT编译器”的配置下实现情况B的这种OSR编译,那么实际执行的时候,就可能有这样的时间轴:

而情况A版的代码,末尾有个奇怪的东西:Runtime.deoptimize()这个伪代码。这是干嘛的呢?

情况A中,我们只编译了当前正在执行的这个循环,循环前和循环后的代码都没有编译。那么如果跳到该版本的代码去跑,跑到循环结束后怎么办?简单,回到低优化层级(例如解释器)去执行就好了嘛。

所以这个Runtime.deoptimize()所代表的意思就是,把低优化层级的执行所需要的状态打包起来传递下去,然后回到低优化层级去继续执行。

例子说到这里想必题主已经获得足够信息来把V8 Crankshaft的OSR机制串起来了。码字太累,先写到这里…

(待续)

后面还可以写几个方面:

Andy Wingo大大以前写过一篇博文讲解当时V8 Crankshaft的OSR机制。等不及我码字的同学请跳传送门:

on-stack replacement in v8 -- wingolog