我对 V8 的点火如何执行我的代码感到困惑。根据我在网上阅读的所有内容,ignition 创建字节码,然后由 Turbofan 创建的程序集存根处理各个字节码指令。所以我期待看到 Ignition 调用这些存根函数的循环。但似乎所有内容都是在单个汇编块中计算的,代码在此处输入: src/execution/execution.cc 位于
Invoke
value = Tagged<Object>(
stub_entry.Call(isolate->isolate_data()->isolate_root(), orig_func,
func, recv, JSParameterCount(params.argc), argv));
这调用了一个函数指针,我假设它指向一些预编译的二进制文件,因为我无法进入它。这似乎可以处理下面更复杂的脚本的整个执行。
我还尝试在 Ignition 生成字节码数组后对其进行调整。这会在程序集黑匣子中的某个位置引发错误。所以看起来字节码指令的处理都是在内置函数中处理的?
如果有人能帮助澄清 Ignition 的工作原理,我将不胜感激!
我正在使用 D8 shell 运行 V8。我正在使用 V8 版本 12.1 并编译为 out/x64.debug。我上面描述的行为是相同的,无论它是一个简单的测试脚本:
const x = 2;
console.log(x);
或者稍微复杂一些的脚本:
const x = [1,2,3];
const y = [4,5,6];
const z = [];
for (let i=0; i<x.length; i++) {
z.push(x[i] + y[i]);
}
console.log(z[0], z[1], z[2]);
(这里是 V8 开发者。)
ignition 创建字节码,然后各个字节码指令由 Turbofan 创建的程序集存根处理。
是的。值得注意的是,这些“程序集存根”(我们称之为“字节码处理程序”)是在 V8 构建时(由
mksnapshot
二进制文件)生成的,并编译成最终的 V8 二进制文件。您可以在它们内部进行中断,但是弄清楚在哪里放置断点有点复杂,而且您将获得的只是一个程序集视图。如果您愿意重新编译 V8,更简单的方法是向您感兴趣的处理程序添加 DebugBreak();
,该处理程序会向其中发出 int3
指令 - 当然,那么您可以仅运行该二进制文件在调试器中,因为未调试的执行将在此时以陷阱终止;您仍然只能获得装配视图。
我期待看到一些 Ignition 调用这些存根函数的循环
Ignition 是一个“间接线程”解释器:每个字节码处理程序都以加载下一个字节码并跳转到相应处理程序的指令序列结束。
这种设计背后的想法是指令序列可能不是随机的:例如,字节码 A 后面可能有很大的机会被字节码 B 跟随。间接线程设计使 CPU 的分支预测器可以轻松了解此类信息情况下,因为(在本例中)A 末尾的间接跳转将(几乎)总是跳转到 B,而其他处理程序末尾的间接跳转可以学习自己最有可能的跳转目标。
理论上,现代 CPU 也可以很好地处理更直观的循环开关解释器设计(这要归功于在分支预测器中考虑了大量的历史记录),但这肯定会让 CPU 的寿命变得更加困难,即
至少
在较弱的CPU设计上可能会有更多的调度开销,甚至可能在任何地方。
没有(见上文),但也没有大的 C++ 循环:字节码处理程序相互尾部调用。
有关更多背景信息,请参阅
v8.dev