CodeStubAssembler 内建函数

本文档旨在作为编写 CodeStubAssembler 内建函数的入门指南,面向 V8 开发人员。

注意: Torque 替代 CodeStubAssembler 成为实现新内建函数的推荐方式。请参阅 Torque 内建函数 获取此指南的 Torque 版本。

内建函数 #

在 V8 中,内建函数可以看作是在运行时由 VM 执行的代码块。一个常见的用例是实现内建对象的函数(例如 RegExp 或 Promise),但内建函数也可以用于提供其他内部功能(例如作为 IC 系统的一部分)。

V8 的内建函数可以使用多种不同的方法实现(每种方法都有不同的权衡)

本文档的其余部分将重点介绍后者,并简要介绍如何开发一个简单的 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(因为我们可以这样做)。

此示例演示了

如果您想在本地进行操作,以下代码基于修订版 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 内建函数具体分为

定义 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());
}