CodeStubAssemblerビルトイン関数
このドキュメントは、CodeStubAssemblerビルトイン関数の記述方法を紹介することを目的としており、V8開発者を対象としています。
注記: Torqueは、新しいビルトイン関数を実装するための推奨方法として、CodeStubAssemblerに取って代わりました。Torqueビルトイン関数で、このガイドのTorque版を参照してください。
ビルトイン関数 #
V8では、ビルトイン関数は、ランタイム時にVMによって実行可能なコードの断片と見なすことができます。一般的なユースケースとしては、ビルトインオブジェクト(RegExpやPromiseなど)の関数を実装することですが、ビルトイン関数は、他の内部機能(例:ICシステムの一部)を提供するためにも使用できます。
V8のビルトイン関数は、いくつかの異なる方法(それぞれトレードオフが異なります)を使用して実装できます。
- プラットフォーム依存のアセンブリ言語:非常に効率的である可能性がありますが、すべてのプラットフォームへの手動移植が必要であり、保守が困難です。
- C++:ランタイム関数と非常に似たスタイルであり、V8の強力なランタイム機能にアクセスできますが、通常はパフォーマンスに敏感な領域には適していません。
- JavaScript:簡潔で読みやすいコード、高速な組み込み関数へのアクセスが可能ですが、遅いランタイム呼び出しの頻繁な使用、型汚染による予測不可能なパフォーマンス、(複雑で分かりにくい)JSセマンティクスに関する微妙な問題があります。
- CodeStubAssembler:アセンブリ言語に非常に近い効率的な低レベルの機能を提供しながら、プラットフォームに依存せず、可読性を維持します。
このドキュメントの残りの部分では、後者について重点的に説明し、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に公開されます。
この例では、以下を示します。
- JS関数のように呼び出すことができる、JavaScriptリンケージを持つCSAビルトイン関数の作成。
- CSAを使用して単純なロジックを実装する:Smiとヒープ数値の処理、条件分岐、TFSビルトイン関数への呼び出し。
- CSA変数の使用。
Math
オブジェクトへのCSAビルトイン関数のインストール。
ローカルで追跡したい場合は、次のコードはリビジョン7a8d20a7に基づいています。
MathIs42
の宣言 #
src/builtins/builtins-definitions.h
のBUILTIN_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ビルトイン関数は、具体的には次のように分類されます。
- TFJ:JavaScriptリンケージ。
- TFS:スタブリンケージ。
- TFC:カスタムインターフェース記述子が必要なスタブリンケージビルトイン関数(引数がタグなしの場合、または特定のレジスタに渡す必要がある場合など)。
- TFH:ICハンドラに使用される特殊なスタブリンケージビルトイン関数。
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
内の順序は重要です。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ビルトイン関数を気にする必要があるのでしょうか?コードをインラインのままにする(または可読性を向上させるためのヘルパーメソッドに抽出する)のはなぜいけないのでしょうか?
重要な理由の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());
}