V8 内置函数(Built-in Functions)完全指南:从 JavaScript 实现到 Runtime 与 CSA 的多种实现路径
【免费下载链接】v8The official mirror of the V8 Git repository项目地址: https://gitcode.com/gh_mirrors/v81/v8
本文基于官方文档 docs/builtin-functions.md 编写,深入剖析 V8 引擎中"内置函数"的多种实现形态:为什么有的内置函数用 JavaScript 写、有的用 C++ 写、有的用汇编写,以及它们之间如何协作。读完本文,你将掌握 V8 内置函数的完整分类体系、
%前缀运行时函数的调用机制、--allow-natives-syntax调试标志的用法,以及如何在源码中定位每一种实现的入口点。
一、什么是 V8 的 Built-in Functions
V8 中,JavaScript 标准库中的绝大多数功能(如Array.prototype.map、Object.create、String.prototype.replace等)并非由用户代码实现,而是由 V8 引擎在启动时预先准备好的一系列内置函数(built-ins)提供。根据 docs/builtin-functions.md 的定义,这些内置函数在实现方式上存在多种不同风格,具体取决于其功能特性、性能要求,以及部分历史演进因素。
这意味着:同样是内置函数,Array.prototype.push和Object.create的底层实现可能完全不同——一个可能直接编译为机器码,另一个可能走 C++ 运行时调用。理解这些差异,是深入阅读 V8 源码、分析性能瓶颈和调试引擎行为的基础。
从当前仓库的源码结构看,内置函数的实现主要集中在以下两个目录:
- src/runtime/:存放运行时函数(Runtime Functions)的声明与实现,核心入口是 src/runtime/runtime.h;
- src/builtins/:存放 built-ins 本身的实现,包括 C++ 实现、CodeStubAssembler(CSA)实现和架构相关汇编实现,核心入口是 src/builtins/builtins.h。
二、内置函数的第一种形态:直接用 JavaScript 实现
部分内置函数直接用 JavaScript 编写,并在运行时像普通用户 JavaScript 一样被解析、编译为可执行代码。这类实现方式通常用于那些逻辑相对复杂、但对极致性能要求不那么苛刻的功能。
这种做法的好处是:
- 开发效率高,语义直观,便于维护;
- 可以利用 V8 的优化编译器(如 Maglev、TurboFan)对其进行自动优化;
- 与用户代码共享同一套优化管线,无需维护独立的底层实现。
在仓库中,这类"用 JavaScript 编写的内置函数"集中体现在 src/builtins/ 目录下的大量.tq(Torque)文件以及部分以 JavaScript 语义定义的 built-in 中。Torque 是 V8 的领域特定语言,其输出会被编译为 CSA 代码或 Turboshaft reducer,最终由 TurboFan 生成机器码——这既保留了"像写 JS 一样写内置函数"的开发体验,又获得了接近手写汇编的性能。关于 Torque 的详细架构可参考 docs/builtins/architecture.md 与 docs/torque/architecture.md。
三、第二种形态:Runtime Functions(运行时函数)
3.1 什么是 Runtime Functions
部分内置函数的功能部分依赖于所谓的运行时函数(runtime functions)。运行时函数是用 C++ 编写的,JavaScript 通过%前缀来调用它们。
例如,在 V8 内部代码中可以看到类似%ArraySpeciesConstructor(...)、%ThrowTypeError(...)的调用——这些都是运行时函数。它们的特点是:
- 拥有完整的 C++ 实现,可以直接访问 V8 引擎的底层内部结构;
- 通过
%前缀从 JavaScript 侧调用; - 通常只限于 V8 内部 JavaScript 代码使用。
3.2 完整的运行时函数清单
所有运行时函数的声明集中维护在 src/runtime/runtime.h 中。该文件通过一组FOR_EACH_INTRINSIC_*宏按功能领域分类列出所有 intrinsic(内置调用原语),例如:
FOR_EACH_INTRINSIC_ARRAY:数组相关,如ArrayIndexOf、ArrayIsArray、GrowArrayElements等;FOR_EACH_INTRINSIC_ATOMICS:Atomics 相关,如AtomicsLoad64、AtomicsStore64、AtomicsCompareExchange等;FOR_EACH_INTRINSIC_BIGINT:BigInt 相关,如BigIntCompareToNumber、BigIntExponentiate等;FOR_EACH_INTRINSIC_CLASSES:class 语义相关,如DefineClass、LoadFromSuper、StoreToSuper等;FOR_EACH_THROWING_INTRINSIC_CLASSES:抛错类,如ThrowConstructorNonCallableError、ThrowStaticPrototypeError等;FOR_EACH_INTRINSIC_COMPILER:编译器相关,如CompileLazy、CompileBaseline、NotifyDeoptimized、ResolvePossiblyDirectEval等;FOR_EACH_INTRINSIC_TIERING:分层编译(tiering)相关,如OptimizeMaglevEager、OptimizeTurbofanEager、StartMaglevOptimizeJob等;FOR_EACH_INTRINSIC_DEBUG:调试相关,如CollectGarbage、DebugOnFunctionCall、HandleDebuggerStatement等;FOR_EACH_INTRINSIC_FORIN:for-in相关,如ForInEnumerate、ForInHasProperty;FOR_EACH_INTRINSIC_FUNCTION:函数相关,如Call、FunctionGetSourceCode、FunctionGetScriptId等;FOR_EACH_INTRINSIC_GENERATOR:生成器/异步函数相关,如AsyncFunctionAwait、AsyncFunctionEnter、CreateJSGeneratorObject等;FOR_EACH_INTRINSIC_INTL:国际化相关(在V8_INTL_SUPPORT开启时生效),如FormatList、StringToLowerCaseIntl等;FOR_EACH_INTRINSIC_INTERNAL:内部通用功能,如AllocateInYoungGeneration、PerformMicrotaskCheckpoint、StackGuard、TerminateExecution、Typeof等。
每个条目通过F(name, nargs, ressize)宏定义,其中:
name:intrinsic 的名称;nargs:参数个数,-1表示可变参数(源码注释中会用/* >= 3 */这类形式标注下限);ressize:返回值个数(绝大多数为 1,返回一个 tagged pointer)。
// src/runtime/runtime.h 中的实际条目示例 F(ArrayIndexOf, 3, 1) // 3 个参数,返回 1 个值 F(NewArray, -1 /* >= 3 */, 1) // 可变参数,至少 3 个 F(ThrowTypeError, -1 /* >= 1 */, 1) // 可变参数,至少 1 个 F(BigIntEqualToBigInt, 2, 1, RuntimeCallProperty::kCannotTriggerGC) // 额外声明:不会触发 GC此外,个别条目会附加RuntimeCallProperty::kCannotTriggerGC属性,声明该调用不会触发垃圾回收,编译器可据此做更激进的优化。
3.3 Intrinsic 的两种暴露形式:%与%_
根据 src/runtime/runtime.h 头部注释,每个 intrinsic 在 JavaScript 中有两种暴露形式:
%name:总是一次运行时调用(对应IntrinsicType::RUNTIME,函数 ID 为Runtime::k##name);%_name(可选):可以被编译器内联,也可以退化为运行时调用,由具体编译器决定(对应IntrinsicType::INLINE,函数 ID 为Runtime::kInline##name)。
无论哪种形式,所有 intrinsic 都拥有一个 C++ 实现,命名统一为Runtime_##name:
#define F(name, nargs, ressize, ...) \ Address Runtime_##name(int args_length, Address* args_object, \ Isolate* isolate); FOR_EACH_INTRINSIC_RETURN_OBJECT(F)而每个编译器(Ignition、Sparkplug、Maglev、TurboFan)都维护一份自己支持的 intrinsic 列表,对于不支持内联的 intrinsic 会回退为普通运行时调用。同时,Runtime::Function结构体(src/runtime/runtime.h)记录了每个 intrinsic 的function_id、类型、JS 名称、C++ 入口地址、参数个数与返回值个数,整个运行时函数表可通过Runtime::RuntimeFunctionTable(isolate)获取。
3.4%前缀的限制与--allow-natives-syntax标志
这些运行时函数通常只限于 V8 内部 JavaScript 代码使用。出于调试目的,如果 V8 以--allow-natives-syntax标志运行,普通 JavaScript 代码中也可以调用它们。
在 src/flags/flag-definitions.h 中可以找到该标志的定义:
DEFINE_BOOL(allow_natives_syntax, false, "allow natives syntax") DEFINE_BOOL(allow_natives_for_differential_fuzzing, false, "allow only natives explicitly allowlisted for differential " "fuzzing") DEFINE_IMPLICATION(allow_natives_for_differential_fuzzing, allow_natives_syntax) DEFINE_IMPLICATION(allow_natives_for_differential_fuzzing, fuzzing)要点:
- 默认值为
false,即普通构建下%语法不可用; allow_natives_for_differential_fuzzing是更受限的变体,只放行明确列入差分模糊测试白名单的 natives,且开启它隐式开启allow_natives_syntax与fuzzing;- 该标志主要用于 V8 自身的测试(尤其是
test/mjsunit/下的测试)与差分模糊测试(differential fuzzing)。
典型用法(以 d8 运行):
# 允许在 JS 代码中使用 % 前缀调用运行时函数 d8 --allow-natives-syntax test.js # 调试标志与常用调试手段组合使用 d8 --allow-natives-syntax --trace-opt test.js在测试代码中,常见做法是利用%OptimizeFunctionOnNextCall、%GetOptimizationStatus等 intrinsic 来手动触发并检查优化状态——Runtime::OptimizationStatus枚举(src/runtime/runtime.h)正是为这类检查设计的,其中的位定义需要与test/mjsunit/mjsunit.js保持同步(源码注释明确说明了这一点)。这就是为什么 mjsunit 测试在头部通常会带上// Flags: --allow-natives-syntax这样的注释。
注意:
Runtime::IsEnabledForFuzzing()(src/runtime/runtime.h)表明并非所有 intrinsic 都向 fuzzer 开放,只有明确放行的子集才可用于差分模糊测试,这与上面的allow_natives_for_differential_fuzzing标志的设计互相印证。
3.5 运行时函数的入口签名
所有运行时函数拥有统一的 C++ 入口签名,且通过RUNTIME_FUNCTION宏展开(定义于 src/runtime/runtime.cc 附近):
Address Runtime_##name(int args_length, Address* args_object, Isolate* isolate);其中args_length为参数数量,args_object为参数数组,isolate为当前隔离区。runtime.h中还提供了一系列查询接口,例如Runtime::NeedsExactContext(id)判断是否依赖"当前上下文"、Runtime::IsNonReturning(id)判断是否必定抛异常(永不正常返回)、Runtime::MayAllocate(id)判断是否可能触发堆分配,这些元信息被编译器和 GC 相关逻辑广泛使用。
四、第三种形态:Built-ins 的多种实现方式
除 JavaScript 实现和 runtime functions 之外,另一些函数被实现为built-ins,而 built-ins 本身又有数种不同的实现手段。根据 docs/builtin-functions.md 与 docs/builtins/architecture.md 的归纳,主要有以下几种。
4.1 平台相关汇编实现(Architecture-Specific Assembler)
部分 built-ins直接用平台相关的汇编(assembly)实现。这类实现位于 src/builtins/ 下的架构子目录中,例如x64、arm64、ia32、riscv、mips64、loong64、ppc、s390等(对应src/builtins/[arch]/builtins-[arch].cc)。
它们通过 V8 的MacroAssembler直接发射原始机器指令,用于:
- 底层入口点(entry points)与跳板(trampolines);
- 需要操纵调用栈的操作(压栈参数、建立解释器栈帧等)——因为 TurboFan 生成的代码假定固定的栈布局,这类操作只能用架构相关 built-in 完成;
- 对每条指令都极为敏感的热点路径。
代价是:必须为每个支持的架构分别编写。在 src/builtins/builtins-definitions.h 中,这类 built-in 用ASM(name, interface_descriptor)宏登记,例如反优化入口ASM(DeoptimizationEntry_Eager, DeoptimizationEntry)、写屏障TFC(RecordWriteSaveFP, WriteBarrier)等。
4.2 CodeStubAssembler(CSA)实现
还有一部分 built-ins 使用CodeStubAssembler(CSA)实现——一个平台无关的抽象层。CSA 提供一套 C++ API,用编程方式直接发射 TurboFan 图节点(如Parameter、Load、Branch、CallRuntime),绕过了 JavaScript 的解析器和 AST,最终由 TurboFan 后端生成机器码。
CSA 的实现文件命名规律为builtins-*-gen.cc/builtins-*.h(位于 src/builtins/)。在 src/builtins/builtins-definitions.h 中,CSA/TurboFan built-ins 按链接约定分为:
| 宏 | 含义 | 参数说明 |
|---|---|---|
CPP | 纯 C++ built-in,通过BUILTIN_EXITframe 进入 | name, formal parameter count |
TFJ | TurboFan 实现,JS 链接(可作为 JavaScript 函数调用) | name, formal parameter count, explicit argument names... |
TFS | TurboFan 实现,CodeStub 链接 | name, needs context, explicit argument names... |
TFC | TurboFan 实现,CodeStub 链接且带自定义描述符 | name, interface descriptor |
TFH | TurboFan 实现的处理器(handler),CodeStub 链接 | name, interface descriptor |
BCH | 字节码处理器(bytecode handler),带字节码分发链接 | name, OperandScale, Bytecode |
ASM | 平台相关汇编 built-in | name, interface descriptor |
同类的TFJ_TSA、TFC_TSA、BCH_TSA则是使用Turboshaft Assembler定义的版本(V8 正逐步从 CSA 迁移到 Turboshaft,参见 docs/builtins/architecture.md)。
built-ins 有时也被用来实现胶水代码(glue code)片段,而不一定是完整函数。例如 src/builtins/builtins-definitions.h 中的LOAD_IC_HANDLER_LIST、LOAD_IC_IN_OBJECT_FIELD_WITH_INDEX_HANDLER_LIST等宏,生成的是大量内联缓存(IC)处理器——它们不是"标准库函数",而是执行属性加载/存储的微型代码片段,每个对应一种字段布局(如 in-object 非 double 字段、out-of-object 字段、来自原型的常量等)。这正是官方文档所说"glue code,不一定是完整函数"的典型例证。
4.3 纯 C++ 实现
还有一部分 built-ins直接用 C++ 实现。这类函数以BUILTIN(Name)宏定义,例如 src/builtins/builtins-array.cc 中的:
BUILTIN(ArrayPrototypeFill) { ... } // src/builtins/builtins-array.cc BUILTIN(ArrayPush) { ... } BUILTIN(ArrayPop) { ... } BUILTIN(ArrayShift) { ... } BUILTIN(ArrayUnshift) { ... } BUILTIN(ArrayConcat) { ... }纯 C++ built-in 的优缺点:
- 优点:最容易编写,拥有完整 C++ 标准库访问权限,可以触及 V8 复杂的内部结构(堆、句柄、对象布局等);
- 缺点:由于需要在 JS 与 C++ 之间跨边界(boundary crossing),性能通常低于 Torque/CSA 实现。
4.4 Builtins 的完整清单与枚举
所有 built-ins 的完整清单由 src/builtins/builtins-definitions.h 中的BUILTIN_LIST(...)宏家族汇总(它同时引入bytecodes-builtins-list.h与torque-generated/builtin-definitions.h),而 src/builtins/builtins.h 则通过BUILTIN_LIST展开出enum class Builtin枚举,并为每个枚举值提供k##Name形式:
enum class Builtin : int32_t { kNoBuiltinId = -1, #define DEF_ENUM(Name, ...) k##Name, BUILTIN_LIST(DEF_ENUM, ...) #undef DEF_ENUM kFirstBytecodeHandler = ..., kFirstLoadICHandler = ..., kLastLoadICHandler = kFirstLoadICHandler + kBuiltinLoadICHandlerCount - 1 };同时,src/builtins/builtins.h 提供便捷宏BUILTIN_CODE(isolate, name),用于在 C++ 侧获取指定 built-in 的 code handle:
#define BUILTIN_CODE(isolate, name) \ (isolate)->builtins()->code_handle(i::Builtin::k##name)Builtins类(src/builtins/builtins.h)负责管理这些 built-in 的加载、反汇编支持(Lookup(Address pc))以及 tier0/tier1 分层(kBuiltinCount、kBuiltinTier0Count等常量)。tier0 built-in保证靠近 root register,可以被高效地以 root-relative 方式调用(src/builtins/builtins-definitions.h 明确指出 tier0 集合必须尽可能小,且不应随意新增)。
五、Built-ins 的加载与嵌入机制
built-ins 在V8 构建过程中(或不使用快照时的启动过程中)被编译,并存储于**快照(Snapshot)**中,从而保证 V8 启动时所有标准库函数已就绪,无需在运行时解析和编译它们。同时,同一进程内的所有 Isolate 与 context共享这些 built-ins(参见 docs/builtins/architecture.md)。
为了降低启动时间并支持多 Isolate 间的内存共享,V8 将 built-ins嵌入二进制文件:
- Embedded Blob:built-ins 被编译并写入二进制的一个专用段(embedded blob);
- 共享:同一进程的多个 Isolate 共享同一份内置可执行代码,节省内存。
在某些条件下,V8 会把嵌入的 built-ins复制或重映射(remap)到某个 Isolate 的CodeRange中:
- 在 x64、arm64 等架构上,函数调用只有有限的相对寻址范围。如果
CodeRange中生成的代码需要调用 built-in,而 embedded blob 在内存中距离太远,就无法使用高效的 PC 相对调用; - 当
v8_flags.short_builtin_calls开启(通常取决于可用物理内存)时,V8 会把这些 built-ins 复制/重映射到CodeRange中,使其靠近生成的代码; - 若操作系统支持页重映射,则采用remap(保持共享,避免制造私有脏页);否则退化为memcpy 复制(会增加私有内存占用)。
六、总结:如何选择实现方式
综合 docs/builtin-functions.md 与 docs/builtins/architecture.md 的说明,可以总结出 V8 内置函数实现方式的选择逻辑:
| 实现方式 | 代表位置 | 优点 | 缺点 | 典型用途 |
|---|---|---|---|---|
| JavaScript(含 Torque) | src/builtins/*.tq | 开发效率高、语义直观、可被优化管线优化 | 需要经过解析/编译 | 标准库方法的主体逻辑 |
| Runtime Functions(C++) | src/runtime/runtime.cc及runtime.h声明 | 完整访问引擎内部、实现复杂语义 | JS/C++ 边界开销 | 抛错、GC 触发、编译器辅助等 |
| CodeStubAssembler / Turboshaft | src/builtins/builtins-*-gen.cc | 平台无关、高性能、贴近底层 | 编写门槛高 | 热点内建方法、IC 处理器、胶水代码 |
| 平台相关汇编 | src/builtins/[arch]/builtins-[arch].cc | 极致性能、可精确操控栈 | 每个架构都要写一份 | 入口点、跳板、栈帧操作 |
| 纯 C++(BUILTIN 宏) | src/builtins/builtins-*.cc | 最容易写、功能完整 | 跨边界性能较差 | 复杂但不极热的标准库函数 |
因此,"built-ins 以多种方式实现"并非历史包袱,而是 V8 在开发效率、平台独立性、性能与底层控制力之间做出的工程权衡。理解这条主线之后,再去看 src/builtins/ 和 src/runtime/ 目录下的源码,就能快速判断每个函数背后的实现路径与性能特征。
延伸阅读
- docs/builtins/architecture.md:built-ins 架构的深入说明(四种 flavor、加载与嵌入机制);
- src/builtins/builtins.h:
Builtin枚举与Builtins类; - src/builtins/builtins-definitions.h:全部 built-in 的宏清单(CPP/TFJ/TFS/TFC/TFH/BCH/ASM 分类);
- src/runtime/runtime.h:全部运行时函数(intrinsic)的宏清单与
Runtime::Function结构; - docs/torque/architecture.md:Torque 语言架构;
- docs/codegen/code-stub-assembler.md:CodeStubAssembler 详解;
- src/flags/flag-definitions.h:
--allow-natives-syntax等调试标志定义。
【免费下载链接】v8The official mirror of the V8 Git repository项目地址: https://gitcode.com/gh_mirrors/v81/v8
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考