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