原文转载自知乎麦子:https://zhuanlan.zhihu.com/p/54465194
前言
本文主要翻译用V8 内置函数用CodeStubAssembler写法介绍,翻译的不好多多指教,原英文链接https://v8.dev/docs/csa-builtins
Builtins
在V8中,builtins可以看作是VM在运行时可执行的代码块。常见的例子是实现内置对象(如RegExp或Promise)的功能,内置函数也可用于提供其他内部功能(例如作为IC的一部分)
V8的builtins可以使用多种不同的方法实现(每种方法都有不同的权衡),如以下4种方式:
1.Platform-dependent assembly language:很高效,但需要手动适配到所有平台,并且难以维护。
2. C++:风格与runtime functions非常相似,可以访问V8强大的运行时功能,但通常不适合性能敏感区域
3.JavaScript:简洁易读的代码,访问快速内在函数,但频繁使用慢速运行时调用,受到类型污染的不可预测的性能,以及(复杂的和非显而易见的)JS语义的问题。
4.CodeStubAssembler:提供高效的低级功能,非常接近汇编语言,同时保持platform-independent和可读性。(这个也是本章详细讲解的)
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.github.io/ecma262/#sec-tostring.
Node* object = /* ... */;
Node* string = ToString(context, object);
CSA内置运行部分TurboFan编译管道(包括块调度和寄存器分配,特别是不通过优化传递),然后发出最终的可执行代码。
Writing a CodeStubAssembler builtin
在本节中,我们将编写一个简单的CSA内置函数,它接受一个参数,并返回它是否等于数字42。内置函数通过将它放在Math对象上而暴露给JS:
这个案例展示了以下技术:
1.使用JavaScript链接创建内置的CSA,可以像JS函数一样调用。
2.使用CSA实现简单逻辑:Smi和堆数处理,条件和对TFS内置的调用。
3.使用CSA变量。
4.存放在Math对象上的CSA。
Declaring MathIs42
Builtins在src / builtins / builtins-definitions.h中的BUILTIN_LIST_BASE宏中声明。要使用JS linkage和一个名为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 linkage.
TFS: Stub linkage.
TFC: Stub linkage builtin requiring a custom interface descriptor (e.g. if arguments are untagged or need to be passed in specific registers).
TFH: Specialized stub linkage builtin used for IC handlers.
Defining MathIs42
内置定义位于src / builtins / builtins - * - http://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);
}
}
Attaching Math.Is42
Builtin objects such as Math are set up mostly in src/bootstrapper.cc (with some setup occurring in .js files). Attaching our new builtin is simple:
// 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
Defining and calling a builtin with stub linkage
CSA builtins也可以使用Stub linkage创建(而不是像上面在MathIs42中使用的JavaScript linkage)。这些内置函数可用于将常用代码提取到可由多个调用者使用的单独代码对象中,而代码仅生成一次。让我们将处理堆数的代码提取到一个名为MathIsHeapNumber42的单独内置函数中,并从MathIs42中调用它。
定义和使用TFS存根很容易。声明在src/builtins/builtins-definitions.
#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调用MathIsHeapNumber42的内置
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的空间。
Testing stub-linkage builtins
即使我们的新内置函数使用非标准(至少非C ++)调用,也可以为它编写测试用例。可以将以下代码添加到test / cctest / compiler / http://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());
}