Original source: https://javascript.plainenglish.io/lets-understand-chrome-v8-chapter-16-what-is-runtime-why-is-it-important-8c808e58843d
Welcome to other chapters of Let’s Understand Chrome V8
From the view of implementation: the builtins are implemented by Runtime, Torque (CodeStubAssembler), JavaScript, and ASM. Below is the official description of the V8:
V8’s builtins can be implemented using a number of different methods (each with different trade-offs):
Platform-dependent assembly language: can be highly efficient, but need manual ports to all platforms and are difficult to maintain.
C++: very similar in style to runtime functions and has access to V8’s powerful runtime functionality, but usually not suited to performance-sensitive areas.
JavaScript: concise and readable code, access to fast intrinsics, but frequent usage of slow runtime calls, subject to unpredictable performance through type pollution, and subtle issues around (complicated and non-obvious) JS semantics.
CodeStubAssembler: provides efficient low-level functionality that is very close to assembly language while remaining platform-independent and preserving readability.
What is Runtime? It is a method for implementing V8 builtins.
Why is it important? If you understand it means that you know the design and implementation of the quarter of the builtins.
In this article, I will talk about the initialization and call of Runtime. By learning them, we will have a comprehensive understanding of the workflow of Runtime.
1. Initialization
Runtime is the foundation component of V8, which is initialized during the V8 startup and is managed by the ExternalReferenceTable which is a pointer array for holding external resources. The below Init() is responsible for the initialization of ExternalReferenceTable.
1. bool Isolate::Init(ReadOnlyDeserializer* read_only_deserializer,
2. StartupDeserializer* startup_deserializer) {
3. TRACE_ISOLATE(init);
4. const bool create_heap_objects = (read_only_deserializer == nullptr);
5. // We either have both or neither.
6. DCHECK_EQ(create_heap_objects, startup_deserializer == nullptr);
7. base::ElapsedTimer timer;
8. //omit...........................
9. handle_scope_implementer_ = new HandleScopeImplementer(this);
10. load_stub_cache_ = new StubCache(this);
11. store_stub_cache_ = new StubCache(this);
12. materialized_object_store_ = new MaterializedObjectStore(this);
13. regexp_stack_ = new RegExpStack();
14. regexp_stack_->isolate_ = this;
15. date_cache_ = new DateCache();
16. heap_profiler_ = new HeapProfiler(heap());
17. interpreter_ = new interpreter::Interpreter(this);
18. compiler_dispatcher_ =
19. new CompilerDispatcher(this, V8::GetCurrentPlatform(), FLAG_stack_size);
20. // Enable logging before setting up the heap
21. logger_->SetUp(this);
22. { // NOLINT
23. ExecutionAccess lock(this);
24. stack_guard()->InitThread(lock);
25. }
26. // SetUp the object heap.
27. DCHECK(!heap_.HasBeenSetUp());
28. heap_.SetUp();
29. ReadOnlyHeap::SetUp(this, read_only_deserializer);
30. heap_.SetUpSpaces();
31. isolate_data_.external_reference_table()->Init(this);
32. //omit...............
33. }
In the above code, lines 10~20 include the initialization of many foundation components, such as Interpreter and compiler_dispatcher. Line 31 is the ExternalReferenceTable that holds Runtime, see below.
1. void ExternalReferenceTable::Init(Isolate* isolate) {
2. int index = 0;
3. // kNullAddress is preserved through serialization/deserialization.
4. Add(kNullAddress, &index);
5. AddReferences(isolate, &index);
6. AddBuiltins(&index);
7. AddRuntimeFunctions(&index);
8. AddIsolateAddresses(isolate, &index);
9. AddAccessors(&index);
10. AddStubCache(isolate, &index);
11. AddNativeCodeStatsCounters(isolate, &index);
12. is_initialized_ = static_cast<uint32_t>(true);
13. CHECK_EQ(kSize, index);
14. }
In the above code, the 7th line is the initialization of the Runtime AddRuntimeFunctions where is below.
1. void ExternalReferenceTable::AddRuntimeFunctions(int* index) {
2. CHECK_EQ(kSpecialReferenceCount + kExternalReferenceCount +
3. kBuiltinsReferenceCount,
4. *index);
5. static constexpr Runtime::FunctionId runtime_functions[] = {
6. #define RUNTIME_ENTRY(name, ...) Runtime::k##name,
7. FOR_EACH_INTRINSIC(RUNTIME_ENTRY)
8. #undef RUNTIME_ENTRY
9. };
10. for (Runtime::FunctionId fId : runtime_functions) {
11. Add(ExternalReference::Create(fId).address(), index);
12. }
13. CHECK_EQ(kSpecialReferenceCount + kExternalReferenceCount +
14. kBuiltinsReferenceCount + kRuntimeReferenceCount,
15. *index);
16. }
The AddRuntimeFunctions has a parameter index. In my V8, there are 468 runtime functions, the first function is at ExternalReferenceTable[index =430], the last one is at ExternalReferenceTable[430+468–1].
As mentioned above, the ExternalReferenceTable is a pointer array, which is not only to hold the Runtime but also other stuff. We will talk about the stuff in the future.
Figure 1 is adding a Runtime function into the ExternalReferenceTable and gives you a chance to watch runtime_functions in the local variable window.
The 11th line Create() creates an entry according to the Runtime ID and finally stores it into the ExternalReferenceTable, see below.
1. ExternalReference ExternalReference::Create(Runtime::FunctionId id) {
2. return Create(Runtime::FunctionForId(id));
3. }
4. //separation........................
5. const Runtime::Function* Runtime::FunctionForId(Runtime::FunctionId id) {
6. return &(kIntrinsicFunctions[static_cast<int>(id)]);
7. }
8. //separation.......................
9. ExternalReference ExternalReference::Create(const Runtime::Function* f) {
10. return ExternalReference(
11. Redirect(f->entry, BuiltinCallTypeForResultSize(f->result_size)));
12. }
The Create() calls the FunctionForId() to return a kIntrinsicFunctions which is defined in below.
#define FUNCTION_ADDR(f) (reinterpret_cast<v8::internal::Address>(f))
#define F(name, number_of_args, result_size) \
{ \
Runtime::k##name, Runtime::RUNTIME, #name, FUNCTION_ADDR(Runtime_##name), \
number_of_args, result_size \
} \
,
#define I(name, number_of_args, result_size) \
{ \
Runtime::kInline##name, Runtime::INLINE, "_" #name, \
FUNCTION_ADDR(Runtime_##name), number_of_args, result_size \
} \
,
static const Runtime::Function kIntrinsicFunctions[] = {
FOR_EACH_INTRINSIC(F) FOR_EACH_INLINE_INTRINSIC(I)};
#undef I
#undef F
What is the kIntrinsicFunctions? Actually, it is an array in which every element is a 6-tuple. In the below 6-tuple, the first is a unique enum ID, the third is a mnemonic which does not help much, the fourth is the function address, and the last two are parameter counter and return counter. As you see, it is adding the Runtime DebugPrint, namely Initialization.
kIntrinsicFunctions []={
//.....................
{
Runtime::kDebugPrint, Runtime::RUNTIME, "DebugPrint", (reinterpret_cast<v8::internal::Address>(Runtime_DebugPrint)),
1, 1
},
//.....................
let's look at the ExternalReferenceTable.
1. class ExternalReferenceTable {
2. public:
3. static constexpr int kSpecialReferenceCount = 1;
4. static constexpr int kExternalReferenceCount =
5. ExternalReference::kExternalReferenceCount;
6. static constexpr int kBuiltinsReferenceCount =
7. #define COUNT_C_BUILTIN(...) +1
8. BUILTIN_LIST_C(COUNT_C_BUILTIN);
9. #undef COUNT_C_BUILTIN
10. static constexpr int kRuntimeReferenceCount =
11. Runtime::kNumFunctions -
12. Runtime::kNumInlineFunctions; // Don't count dupe kInline... functions.
13. static constexpr int kIsolateAddressReferenceCount = kIsolateAddressCount;
14. static constexpr int kAccessorReferenceCount =
15. Accessors::kAccessorInfoCount + Accessors::kAccessorSetterCount;
16. static constexpr int kStubCacheReferenceCount = 12;
17. static constexpr int kStatsCountersReferenceCount =
18. #define SC(...) +1
19. STATS_COUNTER_NATIVE_CODE_LIST(SC);
20. #undef SC
21. //...........omit.........................
22. ExternalReferenceTable() = default;
23. void Init(Isolate* isolate);
24. private:
25. void Add(Address address, int* index);
26. void AddReferences(Isolate* isolate, int* index);
27. void AddBuiltins(int* index);
28. void AddRuntimeFunctions(int* index);
29. void AddIsolateAddresses(Isolate* isolate, int* index);
30. void AddAccessors(int* index);
31. void AddStubCache(Isolate* isolate, int* index);
32. Address GetStatsCounterAddress(StatsCounter* counter);
33. void AddNativeCodeStatsCounters(Isolate* isolate, int* index);
34. STATIC_ASSERT(sizeof(Address) == kEntrySize);
35. Address ref_addr_[kSize];
36. static const char* const ref_name_[kSize];
37. uint32_t is_initialized_ = 0;
38. uint32_t dummy_stats_counter_ = 0;
39. DISALLOW_COPY_AND_ASSIGN(ExternalReferenceTable);
40. };
Line 7-17 defines all Builtins managed by ExternalReferenceTable, namely all Runtimes. Line 25-35 defines all private methods of ExternalReferenceTable. Take a look at line 35, it's the array that holds the Runtime address. The Address is 'using Address = uintptr_t'.
Figure 2 gives three important points, first is the function Add(); second, helps you to watch the variable ref_addr_; last is the call stack which can help you to debug this code.
2. Calling Runtime
In the last article, I wrote a Runtime function — MyRunctimeFunction, and also described Runtime’s definition.
Here, I’ll talk about the CallRuntime which is common usage in bytecodes and which can help you to understand the interaction between bytecode and runtime.
1. template <class... TArgs>
2. TNode<Object> CallRuntime(Runtime::FunctionId function,
3. SloppyTNode<Object> context, TArgs... args) {
4. return CallRuntimeImpl(function, context,
5. {implicit_cast<SloppyTNode<Object>>(args)...});
6. }
7. //.....separation.........................
8. TNode<Object> CodeAssembler::CallRuntimeImpl(
9. Runtime::FunctionId function, TNode<Object> context,
10. std::initializer_list<TNode<Object>> args) {
11. int result_size = Runtime::FunctionForId(function)->result_size;
12. TNode<Code> centry =
13. HeapConstant(CodeFactory::RuntimeCEntry(isolate(), result_size));
14. return CallRuntimeWithCEntryImpl(function, centry, context, args);
15. }
16. //.....separation.........................
17. TNode<Type> HeapConstant(Handle<Type> object) {
18. return UncheckedCast<Type>(UntypedHeapConstant(object));
19. }
20. //...separation.........................
21. TNode<Object> CodeAssembler::CallRuntimeWithCEntryImpl(
22. Runtime::FunctionId function, TNode<Code> centry, TNode<Object> context,
23. std::initializer_list<TNode<Object>> args) {
24. constexpr size_t kMaxNumArgs = 6;
25. DCHECK_GE(kMaxNumArgs, args.size());
26. int argc = static_cast<int>(args.size());
27 auto call_descriptor = Linkage::GetRuntimeCallDescriptor(zone(), function, argc, Operator::kNoProperties,
Runtime::MayAllocate(function) ? CallDescriptor::kNoFlags
: CallDescriptor::kNoAllocate);
28. for (auto arg : args) inputs.Add(arg);
29. inputs.Add(ref);
30. inputs.Add(arity);
31. inputs.Add(context);
32. CallPrologue();
33. Node* return_value =
34. raw_assembler()->CallN(call_descriptor, inputs.size(), inputs.data());
35. HandleException(return_value);
36. CallEpilogue();
37. return UncheckedCast<Object>(return_value);
38. }
In line 2, the first parameter is FunctionID which is the enum ID motioned above; the second is the current context that will be explained in the future; the last is an args list passed to a specific Runtime function.
In CallRuntimeImpl, line 11 takes out the result size where is stored in the kIntrinsicFunctions; line14 calls CallRuntimeWithCEntryImpl.
In CallRuntimeWithCEntryImpl, line 26 counts the arguments; line 28-31 adds the arguments and context to the inputs array; line 33 calls the specific Runtime.
Note: Sea of Nodes is the prerequisite knowledge if you want to fully understand the principle of CallRuntimeImpl.
Debugging CallRuntimeImpl can only be done in the assembly environment, I have some experience and skills but is boring and complicated, if you want to know, plz contact me.
Okay, that wraps it up for this share. I’ll see you guys next time, take care!
Please reach out to me if you have any issues. WeChat: qq9123013 Email: v8blink@outlook.com
Top comments (0)