CodeStubAssembler 内建函数
本文档旨在作为编写 CodeStubAssembler 内建函数的入门指南,面向 V8 开发人员。
注意: Torque 替代 CodeStubAssembler 成为实现新内建函数的推荐方式。请参阅 Torque 内建函数 获取此指南的 Torque 版本。
内建函数 #
在 V8 中,内建函数可以看作是在运行时由 VM 执行的代码块。一个常见的用例是实现内建对象的函数(例如 RegExp 或 Promise),但内建函数也可以用于提供其他内部功能(例如作为 IC 系统的一部分)。
V8 的内建函数可以使用多种不同的方法实现(每种方法都有不同的权衡)
- 平台相关的汇编语言:可以非常高效,但需要手动移植到所有平台,并且难以维护。
- C++:在风格上与运行时函数非常相似,并且可以访问 V8 强大的运行时功能,但通常不适合性能敏感的区域。
- JavaScript:简洁易读的代码,可以访问快速内联函数,但经常使用缓慢的运行时调用,容易受到类型污染导致性能不可预测,以及围绕(复杂且不明显的)JS 语义的微妙问题。
- CodeStubAssembler:提供高效的低级功能,非常接近汇编语言,同时保持平台无关性和可读性。
本文档的其余部分将重点介绍后者,并简要介绍如何开发一个简单的 CodeStubAssembler (CSA) 内建函数,并将其公开给 JavaScript。
CodeStubAssembler #
V8 的 CodeStubAssembler 是一种自定义的、平台无关的汇编器,它提供低级原语作为汇编的薄抽象层,但也提供了一个广泛的高级功能库。
// Low-level:
// Loads the pointer-sized data at addr into value.
Node* addr = /* ... */;
Node* value = Load(MachineType::IntPtr(), addr);
// And high-level:
// Performs the JS operation ToString(object).
// ToString semantics are specified at https://tc39.es/ecma262/#sec-tostring.
Node* object = /* ... */;
Node* string = ToString(context, object);
CSA 内建函数通过 TurboFan 编译管道的部分运行(包括块调度和寄存器分配,但没有经过优化过程),然后发出最终的可执行代码。
编写 CodeStubAssembler 内建函数 #
在本节中,我们将编写一个简单的 CSA 内建函数,它接受一个参数,并返回它是否表示数字 42
。该内建函数通过将其安装在 Math
对象上公开给 JS(因为我们可以这样做)。
此示例演示了
- 使用 JavaScript 联动创建 CSA 内建函数,可以像 JS 函数一样调用。
- 使用 CSA 实现简单的逻辑:Smi 和堆数字处理、条件语句以及对 TFS 内建函数的调用。
- 使用 CSA 变量。
- 在
Math
对象上安装 CSA 内建函数。
如果您想在本地进行操作,以下代码基于修订版 7a8d20a7。
声明 MathIs42
#
内建函数在 src/builtins/builtins-definitions.h
中的 BUILTIN_LIST_BASE
宏中声明。要使用 JS 联动创建一个名为 X
的新 CSA 内建函数,请使用
#define BUILTIN_LIST_BASE(CPP, API, TFJ, TFC, TFS, TFH, ASM, DBG) \
// […snip…]
TFJ(MathIs42, 1, kX) \
// […snip…]
请注意,BUILTIN_LIST_BASE
接受几个不同的宏,这些宏表示不同的内建函数类型(有关更多详细信息,请参阅内联文档)。CSA 内建函数具体分为
- TFJ:JavaScript 联动。
- TFS:Stub 联动。
- TFC:Stub 联动内建函数,需要自定义接口描述符(例如,如果参数未标记或需要在特定寄存器中传递)。
- TFH:用于 IC 处理程序的专用 Stub 联动内建函数。
定义 MathIs42
#
内建函数定义位于 src/builtins/builtins-*-gen.cc
文件中,大致按主题组织。由于我们将编写一个 Math
内建函数,因此我们将把我们的定义放在 src/builtins/builtins-math-gen.cc
中。
// TF_BUILTIN is a convenience macro that creates a new subclass of the given
// assembler behind the scenes.
TF_BUILTIN(MathIs42, MathBuiltinsAssembler) {
// Load the current function context (an implicit argument for every stub)
// and the X argument. Note that we can refer to parameters by the names
// defined in the builtin declaration.
Node* const context = Parameter(Descriptor::kContext);
Node* const x = Parameter(Descriptor::kX);
// At this point, x can be basically anything - a Smi, a HeapNumber,
// undefined, or any other arbitrary JS object. Let’s call the ToNumber
// builtin to convert x to a number we can use.
// CallBuiltin can be used to conveniently call any CSA builtin.
Node* const number = CallBuiltin(Builtins::kToNumber, context, x);
// Create a CSA variable to store the resulting value. The type of the
// variable is kTagged since we will only be storing tagged pointers in it.
VARIABLE(var_result, MachineRepresentation::kTagged);
// We need to define a couple of labels which will be used as jump targets.
Label if_issmi(this), if_isheapnumber(this), out(this);
// ToNumber always returns a number. We need to distinguish between Smis
// and heap numbers - here, we check whether number is a Smi and conditionally
// jump to the corresponding labels.
Branch(TaggedIsSmi(number), &if_issmi, &if_isheapnumber);
// Binding a label begins generating code for it.
BIND(&if_issmi);
{
// SelectBooleanConstant returns the JS true/false values depending on
// whether the passed condition is true/false. The result is bound to our
// var_result variable, and we then unconditionally jump to the out label.
var_result.Bind(SelectBooleanConstant(SmiEqual(number, SmiConstant(42))));
Goto(&out);
}
BIND(&if_isheapnumber);
{
// ToNumber can only return either a Smi or a heap number. Just to make sure
// we add an assertion here that verifies number is actually a heap number.
CSA_ASSERT(this, IsHeapNumber(number));
// Heap numbers wrap a floating point value. We need to explicitly extract
// this value, perform a floating point comparison, and again bind
// var_result based on the outcome.
Node* const value = LoadHeapNumberValue(number);
Node* const is_42 = Float64Equal(value, Float64Constant(42));
var_result.Bind(SelectBooleanConstant(is_42));
Goto(&out);
}
BIND(&out);
{
Node* const result = var_result.value();
CSA_ASSERT(this, IsBoolean(result));
Return(result);
}
}
附加 Math.Is42
#
Math
等内建对象主要在 src/bootstrapper.cc
中设置(在 .js
文件中进行一些设置)。附加我们新的内建函数很简单
// Existing code to set up Math, included here for clarity.
Handle<JSObject> math = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, math, DONT_ENUM);
// […snip…]
SimpleInstallFunction(math, "is42", Builtins::kMathIs42, 1, true);
现在 Is42
已附加,可以从 JS 中调用它
$ out/debug/d8
d8> Math.is42(42);
true
d8> Math.is42('42.0');
true
d8> Math.is42(true);
false
d8> Math.is42({ valueOf: () => 42 });
true
定义和调用具有 Stub 联动的内建函数 #
CSA 内建函数也可以使用 Stub 联动创建(而不是我们上面在 MathIs42
中使用的 JS 联动)。此类内建函数对于将常用代码提取到单独的代码对象中很有用,这些代码对象可以被多个调用者使用,而代码只生成一次。让我们将处理堆数字的代码提取到一个名为 MathIsHeapNumber42
的单独内建函数中,并从 MathIs42
中调用它。
定义和使用 TFS Stub 很容易;声明再次放在 src/builtins/builtins-definitions.h
中
#define BUILTIN_LIST_BASE(CPP, API, TFJ, TFC, TFS, TFH, ASM, DBG) \
// […snip…]
TFS(MathIsHeapNumber42, kX) \
TFJ(MathIs42, 1, kX) \
// […snip…]
请注意,目前,BUILTIN_LIST_BASE
中的顺序很重要。由于 MathIs42
调用 MathIsHeapNumber42
,因此前者需要列在后者之后(此要求应该在某个时候取消)。
定义也很简单。在 src/builtins/builtins-math-gen.cc
中
// Defining a TFS builtin works exactly the same way as TFJ builtins.
TF_BUILTIN(MathIsHeapNumber42, MathBuiltinsAssembler) {
Node* const x = Parameter(Descriptor::kX);
CSA_ASSERT(this, IsHeapNumber(x));
Node* const value = LoadHeapNumberValue(x);
Node* const is_42 = Float64Equal(value, Float64Constant(42));
Return(SelectBooleanConstant(is_42));
}
最后,让我们从 MathIs42
中调用我们的新内建函数
TF_BUILTIN(MathIs42, MathBuiltinsAssembler) {
// […snip…]
BIND(&if_isheapnumber);
{
// Instead of handling heap numbers inline, we now call into our new TFS stub.
var_result.Bind(CallBuiltin(Builtins::kMathIsHeapNumber42, context, number));
Goto(&out);
}
// […snip…]
}
为什么您应该关心 TFS 内建函数?为什么不将代码保留在内联中(或提取到辅助方法中以提高可读性)?
一个重要的原因是代码空间:内建函数在编译时生成并包含在 V8 快照中,因此在每个创建的隔离区中都会无条件地占用(大量)空间。将大量常用代码提取到 TFS 内建函数中可以快速节省 10 到 100 KB 的空间。
测试 Stub 联动内建函数 #
即使我们的新内建函数使用非标准(至少是非 C++)调用约定,也可以为它编写测试用例。以下代码可以添加到 test/cctest/compiler/test-run-stubs.cc
中,以在所有平台上测试该内建函数
TEST(MathIsHeapNumber42) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
Heap* heap = isolate->heap();
Zone* zone = scope.main_zone();
StubTester tester(isolate, zone, Builtins::kMathIs42);
Handle<Object> result1 = tester.Call(Handle<Smi>(Smi::FromInt(0), isolate));
CHECK(result1->BooleanValue());
}