261004.7 · deps: update V8 to 15.2.124.38
Function Architecture in V8
To execute JavaScript functions efficiently, V8 splits function data into context-independent and context-dependent parts. This separation allows sharing of heavy assets (like bytecode) while allowing specific instances (closures) to maintain their own state and feedback.
This document describes this architecture and how functions are chained together both statically and dynamically.
Context-Independent Data: SharedFunctionInfo
Data that does not change regardless of where or how many times a function is instantiated is stored in the SharedFunctionInfo (SFI).
- Purpose: To save memory by sharing read-only information across all instances of the function, even across different
NativeContexts. - Key Contents:
- Bytecode: The executable bytecode generated by Ignition.
ScopeInfo: The lexical structure of the scope (see Scopes and ScopeInfos).- Source Positions: Mapping bytecode to source code lines.
FeedbackMetadata: Describes the shape and size of theFeedbackVectorthat needs to be allocated for this function.
SFI Chaining via Bytecode
SharedFunctionInfo objects do not form a direct linked list. Instead, they form a tree structure embedded within the bytecode.
When a function contains a nested function definition, the outer function’s BytecodeArray is responsible for creating the closure for the inner function.
- Inner SFI in Constant Pool: The SFI for the inner function is created during parsing/compilation of the outer function and is placed in the Constant Pool of the outer function’s
BytecodeArray. CreateClosureBytecode: The outer function’s bytecode contains aCreateClosureinstruction at the point where the inner function is defined/created.- Reference: The
CreateClosurebytecode takes an operand that is the index of the inner SFI in the constant pool.
Context-Dependent Data
Data that depends on the specific execution context or function instance is stored in objects linked from the JSFunction.
JSFunction
A JSFunction represents an actual JavaScript function object available to the user code (a closure).
- Purpose: To represent a specific instance of a function with its own scope chain and execution state.
- Key Contents:
- Link to
SharedFunctionInfo: Points to the shared data. Context: Points to the execution context (lexical environment) where the function was created. This is what makes it a closure.FeedbackCell: Points to a cell that holds theFeedbackVector.JSDispatchHandle: In modern V8, this is a 32-bit index into theJSDispatchTable, used to find the executable code, enabling efficient tiering.
- Link to
FeedbackCell
The FeedbackCell acts as a level of indirection between the JSFunction and the FeedbackVector.
- Purpose:
- Tiering Control: It stores the
interrupt_budget, which is decremented on function entry and backward branches. When it reaches zero, it triggers a tiering request. - Lazy Allocation: It allows V8 to delay the allocation of the full
FeedbackVectoruntil the function becomes “hot”. - Sharing: In some cases, multiple closures can share the same
FeedbackCellif they are guaranteed to have the same feedback behavior.
- Tiering Control: It stores the
FeedbackVector
The FeedbackVector holds the runtime feedback (Inline Caches) used by the optimizing compilers.
- Purpose: To collect data about the types and shapes of objects the function operates on.
- Key Contents:
- Slots: Array of slots, each corresponding to an operation in the bytecode.
ClosureFeedbackCellArray: HoldsFeedbackCells for inner closures.
- Context Dependency: Feedback is highly dependent on the types encountered in a specific context. Sharing feedback across different contexts with different types would lead to polymorphism and deoptimizations.
FeedbackVector Chaining via CreateClosure Slots
The dynamic chaining happens through the feedback system, specifically to allow inner functions to have their own feedback vectors when they become hot.
ClosureFeedbackCellArray: EveryFeedbackVector(and also uncompiledFeedbackCells that are preparing for vector allocation) contains aClosureFeedbackCellArray.- Slot per Closure: For every
CreateClosurebytecode in a function, there is a corresponding slot in itsClosureFeedbackCellArray. - Pre-allocated FeedbackCells: This array is populated with
FeedbackCells before the closures are actually created. - Passing the Cell: When
CreateClosureis executed, it looks up the pre-allocatedFeedbackCellfrom the outer function’sClosureFeedbackCellArrayand links it to the newJSFunction.
This creates a runtime chain:
Outer JSFunction -> Outer FeedbackVector -> ClosureFeedbackCellArray -> Inner FeedbackCell -> Inner FeedbackVector (once allocated).
Modern V8 Features
Sharing of JSDispatchHandle
- Initialization: Each
FeedbackCellinClosureFeedbackCellArrayis allocated aJSDispatchHandle(initially pointing toCompileLazy). - Closure Creation:
CreateClosurecopies theJSDispatchHandlefrom theFeedbackCellinto the newJSFunction. - Shared Dispatch: Closures created from the same site share the same
JSDispatchTableentry. - Efficient Tiering: Updating the code pointer in the table entry updates all sharing closures immediately.
FeedbackCell State Transitions
V8 tracks the number of closures created from a site using the Map of the FeedbackCell:
NoClosuresCellMap->OneClosureCellMap->ManyClosuresCellMapThese help decide when to allocate aFeedbackVectorand manage specialized code.
Concrete Example
Consider the following code:
// Top-level script
function outer() {
function inner() {
return 42;
}
return inner;
}
outer(); // Call to outerObject Graph
Here is what the object graph looks like after the top-level script has been executed and outer() has been called, but before inner() is called.
dot is not installed; install graphviz to draw this graph
digraph G {
rankdir=TB;
newrank=true;
node [shape=record];
edge [];
subgraph cluster_static {
label="Static Structure (SFIs)";
TL_SFI [label="Top-level Script (SFI)"];
Outer_SFI [label="outer (SFI)"];
Inner_SFI [label="inner (SFI)"];
TL_SFI -> Outer_SFI [label="via Bytecode's\nConstant Pool"];
Outer_SFI -> Inner_SFI [label="via Bytecode's\nConstant Pool"];
}
subgraph cluster_dynamic {
label="Dynamic Instances (Runtime)";
TL_JSF [label="Top-level Script (JSFunction)"];
TL_FC [label="FeedbackCell"];
CC_TL [label="CompileScript"];
TL_FV [label="FeedbackVector"];
Outer_FC [label="FeedbackCell\n(for outer)"];
CC_Outer [label="CreateClosure\n(Outer)"];
Outer_JSF [label="outer (JSFunction)"];
Outer_FV [label="FeedbackVector"];
Inner_FC [label="FeedbackCell\n(for inner)"];
CC_Inner [label="CreateClosure\n(Inner)"];
Inner_JSF [label="inner (JSFunction)"];
Inner_FV [label="FeedbackVector\nor Undefined"];
TL_FC -> TL_JSF [style=invis];
TL_FC -> CC_TL [label="consumed by", style=dashed];
CC_TL -> TL_JSF [label="creates", style=dashed];
TL_JSF -> TL_FC;
TL_FC -> TL_FV;
TL_FV -> Outer_FC [label="contains"];
Outer_FC -> Outer_JSF [style=invis];
Outer_FC -> CC_Outer [label="consumed by", style=dashed];
CC_Outer -> Outer_JSF [label="creates", style=dashed];
Outer_JSF -> Outer_FC;
Outer_FC -> Outer_FV;
Outer_FV -> Inner_FC [label="contains"];
Inner_FC -> Inner_JSF [style=invis];
Inner_FC -> CC_Inner [label="consumed by", style=dashed];
CC_Inner -> Inner_JSF [label="creates", style=dashed];
Inner_JSF -> Inner_FC;
Inner_FC -> Inner_FV;
}
TL_SFI -> CC_TL [label="consumed by", style=dashed];
Outer_SFI -> CC_Outer [label="consumed by", style=dashed];
Inner_SFI -> CC_Inner [label="consumed by", style=dashed];
TL_JSF -> TL_SFI;
Outer_JSF -> Outer_SFI;
Inner_JSF -> Inner_SFI;
TL_JSF -> Outer_JSF [style=invis];
Outer_JSF -> Inner_JSF [style=invis];
{ rank="same"; TL_SFI; TL_FC; }
{ rank="same"; Outer_SFI; Outer_FC; }
{ rank="same"; Inner_SFI; Inner_FC; }
}
Scope and Context Graph
This graph shows how ScopeInfo (static) and Context (dynamic) are linked.
dot is not installed; install graphviz to draw this graph
digraph G {
rankdir=TB;
newrank=true;
node [shape=record];
edge [];
subgraph cluster_static {
label="Static Structure (Scopes)";
TL_SFI [label="Top-level Script (SFI)"];
TL_SI [label="Top-level ScopeInfo"];
Outer_SFI [label="outer (SFI)"];
Outer_SI [label="outer ScopeInfo"];
Inner_SFI [label="inner (SFI)"];
Inner_SI [label="inner ScopeInfo"];
TL_SFI -> TL_SI;
Outer_SFI -> Outer_SI [constraint=false];
Inner_SFI -> Inner_SI [constraint=false];
Inner_SI -> Outer_SI -> TL_SI [label="parent"];
}
subgraph cluster_dynamic {
label="Dynamic Instances (Contexts)";
TL_JSF [label="Top-level Script (JSFunction)"];
TL_Ctx [label="Top-level Context"];
Outer_JSF [label="outer (JSFunction)"];
Outer_Ctx [label="outer Context"];
Inner_JSF [label="inner (JSFunction)"];
Inner_Ctx [label="inner Context"];
TL_JSF -> TL_Ctx;
Outer_JSF -> Outer_Ctx [constraint=false];
Inner_JSF -> Inner_Ctx [constraint=false];
Inner_Ctx -> Outer_Ctx -> TL_Ctx [label="parent"];
}
TL_Ctx -> TL_SI [constraint=false];
Outer_Ctx -> Outer_SI [constraint=false];
Inner_Ctx -> Inner_SI [constraint=false];
TL_JSF -> TL_SFI [constraint=false];
Outer_JSF -> Outer_SFI [constraint=false];
Inner_JSF -> Inner_SFI [constraint=false];
TL_SFI -> Outer_SFI -> Inner_SFI [style=invis];
TL_JSF -> Outer_JSF -> Inner_JSF [style=invis];
TL_SI -> Outer_SI -> Inner_SI [style=invis];
TL_Ctx -> Outer_Ctx -> Inner_Ctx [style=invis];
}
Developer Guide: Tracing JSFunction Initialization
Key Entry Points
JSFunction::CreateAndAttachFeedbackVectorJSFunction::EnsureClosureFeedbackCellArrayFastNewClosure(Builtin)