CodeStubAssemblerビルトイン関数

このドキュメントは、CodeStubAssemblerビルトイン関数の記述方法を紹介することを目的としており、V8開発者を対象としています。

注記: Torqueは、新しいビルトイン関数を実装するための推奨方法として、CodeStubAssemblerに取って代わりました。Torqueビルトイン関数で、このガイドのTorque版を参照してください。

ビルトイン関数 #

V8では、ビルトイン関数は、ランタイム時にVMによって実行可能なコードの断片と見なすことができます。一般的なユースケースとしては、ビルトインオブジェクト(RegExpやPromiseなど)の関数を実装することですが、ビルトイン関数は、他の内部機能(例:ICシステムの一部)を提供するためにも使用できます。

V8のビルトイン関数は、いくつかの異なる方法(それぞれトレードオフが異なります)を使用して実装できます。

このドキュメントの残りの部分では、後者について重点的に説明し、JavaScriptに公開される単純なCodeStubAssembler(CSA)ビルトイン関数の開発に関する簡単なチュートリアルを示します。

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ビルトイン関数の記述 #

このセクションでは、単一の引数を取り、それが数値42を表すかどうかを返す単純なCSAビルトイン関数を記述します。このビルトイン関数は、(可能なので)MathオブジェクトにインストールすることでJSに公開されます。

この例では、以下を示します。

ローカルで追跡したい場合は、次のコードはリビジョン7a8d20a7に基づいています。

MathIs42の宣言 #

src/builtins/builtins-definitions.hBUILTIN_LIST_BASEマクロでは、ビルトイン関数が宣言されます。JSリンケージとXという名前の1つのパラメータを持つ新しい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

スタブリンケージを持つビルトイン関数の定義と呼び出し #

CSAビルトイン関数は、(上記MathIs42で使用したJSリンケージではなく)スタブリンケージを使用して作成することもできます。このようなビルトイン関数は、一般的に使用されるコードを個別のコードオブジェクトに抽出するために役立ち、複数の呼び出し元で使用できますが、コードは一度だけ生成されます。ヒープ数値を処理するコードをMathIsHeapNumber42という別のビルトイン関数に抽出し、MathIs42から呼び出してみましょう。

TFSスタブの定義と使用は簡単です。宣言は、再び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内の順序は重要です。MathIs42MathIsHeapNumber42を呼び出すため、前者は後者よりも後にリストする必要があります(この要件は、ある時点で解除されるはずです)。

定義も簡単です。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ビルトイン関数を気にする必要があるのでしょうか?コードをインラインのままにする(または可読性を向上させるためのヘルパーメソッドに抽出する)のはなぜいけないのでしょうか?

重要な理由の1つはコードサイズです。ビルトイン関数はコンパイル時に生成され、V8スナップショットに含まれるため、作成されたすべてのisolateで(かなりの)容量を無条件に消費します。一般的に使用される大きなコードチャンクをTFSビルトイン関数に抽出すると、10KB〜100KBの容量節約に迅速につながる可能性があります。

スタブリンケージビルトイン関数のテスト #

新しいビルトイン関数は標準的ではない(少なくとも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());
}