【问题标题】:Does V8 monitor the execution of optimized machine code?V8 是否监控优化机器代码的执行?
【发布时间】:2022-01-25 00:15:44
【问题描述】:

AFAIK,V8 中大致有两种代码对象。一种是由 Ignition 解释的 javascript 字节码,另一种是由 Turbofan 编译和优化的机器码。根据execution/frames.h,V8 为每种代码对象构造了不同的堆栈帧。这意味着 V8 在执行之前应该知道callee 的类型。

当未优化的代码(JS 字节码)调用优化的代码时,我猜 Ignition 可以正确处理这种情况,为优化的代码构建一个新的堆栈帧。但是,当优化后的代码(机器码)调用未优化的代码时,我很好奇 V8 是如何确定 callee 是否未优化的。如果机器代码直接在处理器上运行,没有什么可以帮助 V8 确定callee 的种类。

另外,在我的理解中,V8 应该检测某些代码依赖是否受到损害,以标记或取消优化无效的代码对象。如果 V8 不监控机器代码的执行,这似乎也不可行。

所以我的问题是:

  1. V8 是否监控(优化的)机器代码的执行?如果是这样,它是如何发生的?

  2. 如果1为false,那么V8如何检查代码依赖失效或检测callee是否编译?

【问题讨论】:

  • “监控”是什么意思?
  • "没有什么可以帮助 V8 在执行调用指令之前确定被调用者的类型。" - 为什么需要它?设置新的堆栈帧是被调用函数(或者可能是call 本身)的工作,它不需要在执行call 指令之前发生。
  • @user2864740 据我所知,Turbofan 在编译期间会将依赖项安装到代码对象中。(请参阅 DependencyGroup@code.h)。如果依赖项之一被破坏(无效),则代码被取消优化或将被取消优化。
  • @Bergi 我的意思是字面上的执行监控。如果 V8 不监控机器代码的执行,那么 V8 是如何在执行时检测到某些代码依赖的失效呢?比如我们看test/mjsunit/compiler/deopt-lazy-shape-mutation.js,假设change_o被优化了,那么v8怎么知道f返回时应该去优化。
  • @SeungwanKwon 您可能对these slides 感兴趣,它详细介绍了该示例;)(虽然它已经过时,但它仍然对实时修补的概念提供了一些概述)

标签: javascript compiler-construction v8 jit execution


【解决方案1】:

(这里是 V8 开发人员。)

V8 为每种代码对象构造不同的堆栈帧。这意味着 V8 在执行之前应该知道被调用者的类型。

堆栈框架由需要它们的人设置,因此调用者不必检查被调用者。 (他们可以,如果必须的话。)

如果机器代码直接在处理器上运行,没有什么可以帮助 V8 确定被调用者的类型。

V8 可以发出检查被调用者类型的机器代码。但重复我自己:知道被调用者的优化状态是没有必要的。

V8 是否监控(优化的)机器代码的执行?

不,它没有。

V8如何检查代码依赖失效

依赖项安装在可以更改的东西上(例如地图、保护器)。当该更改发生时,已注册的代码依赖项将被取消优化(即,当它们在堆栈上激活时标记为“延迟取消优化”,否则就被丢弃)。

【V8如何检测被调用者是否编译?

不需要。当一个函数还没有被编译时,它的“代码”将是一个触发编译的存根。

【讨论】:

  • 所以在某种程度上它是被“监控”的,因为去优化将scan the stack 用于无效函数,对吧?
  • 什么是“标记为“惰性去优化””,它是否像反向惰性编译一样工作?即他们的代码被一个存根替换,该存根将堆栈帧和调用转换为解释器?
  • @JonasWilms:我不会称之为“监控”,但如果你想这样称呼它,那很好。 ——Bergi:基本上是的。有关详细信息,请参阅 Jonas 刚刚链接的文档。一个关键点是“延迟删除”仅针对堆栈上的非最顶层激活(最顶层,即当前正在执行的函数只会执行“急切删除”)。
  • @JonasWilms:反优化函数不会影响其调用者。下次执行此类调用时,它将转到该函数的未优化(或重新优化)代码。您可以将其视为具有内部 .code 属性的函数对象,该属性会根据需要重新分配,并在每次调用该函数时读取。当然,如果一个函数被 inlined 到它的调用者中并且需要 deopt,那么只有一个组合代码对象要被丢弃——这就是过多内联会造成更大伤害的原因之一比好。
  • 在示例中,o 最初被假定为常量并被如此注释。当f 被优化并假设o 是常量时,它被注册为o 的依赖项。当change_o 重新分配o 时,它看到o 到目前为止被假定为常量,因此它将其所有依赖项标记为去优化,并将o 标记为非常量。 f 的惰性 deopt 将在 change_o 返回后立即发生(即当 f 成为最顶层的堆栈帧时)。换句话说:B 本身将 A 标记为去优化;没有任何第三方实体必须观察 B 在做什么。
【解决方案2】:

查看您在 cmets 中给出的具体示例:

var b = false;

function change_o() {
  // as long as b is false, this won't be reassigned ...
  if (b) o = { y : 1, x : 0};
}

var o = { x : 1 };

function f() {
  change_o();
  // ... and thus o.x can be compiled down
  return o.x;
}

// f and change_o are compiled somewhen 
f(); f(); f(); // ...

// a precondition changes
b = true;

// during execution of change_o o is reassigned and changes its shape, accessing o.x will now need a different offset and thus f (which is currently on the stack) becomes invalid
f();

因此,在分配o = 发生的那一刻,运行时跳转到一些去优化逻辑,该逻辑必须基于错误假设使所有代码无效。使从非编译代码到编译代码的调用无效很容易,只需从某种注册表中删除对编译代码的引用,没有人会再调用它。使来自已编译函数的未来调用无效到已失效的已编译函数更难,但如果调用使用某种间接,则可以重写此间接以重定向所有未来的调用。 这留下了两种失效问题:

  • 当前评估的函数需要失效(-> 急切的去优​​化),这也不是问题,因为它是退出优化例程的函数
  • 调用评估函数的函数需要失效(-> 延迟去优化),这是有问题的,因为它们可能在堆栈中的某个位置,并且在某些时候继续执行会恢复到它们(例如示例中的 f以上)

但是在后一种情况下,有一种方法可以查找和替换这些函数: 每次一个函数calls 到另一个函数时,它会将返回地址压入堆栈(参见例如x86 stack usage),当被调用函数执行ret 指令时,它会弹出地址并跳转到那里。因此,通过扫描堆栈以查找位于无效函数内部的返回地址,并将其替换为另一个地址,当像change_o 这样的内部函数返回到f 时,它实际上并没有跳回到编译后的f 代码,而是跳转到处理程序,然后可以进行必要的调整。

所以 V8 不会“监控”执行,它会在必要时通过重写堆栈来主动修改它。

可以在here找到详细的演练。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-29
    • 1970-01-01
    • 2016-01-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多