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

Does V8 monitor the execution of optimized machine code?

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

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

的种类

另外,在我看来,V8 应该检测是否有一些代码依赖被破坏,以标记或取消优化无效的代码对象。如果V8不监控机器码的执行,似乎也不可行。

所以我的问题是:

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

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

(此处为 V8 开发人员。)

V8 constructs a different stackframe for each kind of code object. It means V8 should know the kind of the callee before it executes.

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

If the machine code is run directly on the processor, nothing can help V8 determine the kind of callee.

V8 可以发出机器代码来检查被调用者的类型。但重复一遍:不需要知道被调用者的优化状态。

Does V8 monitor the execution of (optimized) machine code?

不,不是。

how does V8 check the invalidation of code dependency

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

[how does V8] detect whether the callee is compiled or not?

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

看你在评论中给出的具体例子:

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 = 发生的那一刻,运行时跳入了一些反优化逻辑,这必须使所有基于错误假设的代码无效。使 non-compiled 代码对编译代码的调用无效很容易,只需从某种注册表中删除对编译代码的引用,就不会再调用它了。使已编译函数对已无效编译函数的未来调用无效更难,但如果调用使用某种间接,则可以重写此间接以重定向所有未来调用。 这留下了两种无效问题:

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

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

因此 V8 不会“监视”执行,它会在必要时通过重写堆栈主动修改它。

可以找到详细的演练 here