Skip to content

Performance and Benchmarks

Liu.Yandong.Hanks edited this page Aug 26, 2026 · 9 revisions

性能与基准测试

运行可复现的基准测试、对比记录的基线,并优化已测量的热路径。

适用于 4.0.0

首页 · 语言指南

运行基准测试

统一基准测试项目位于 benchmark/,覆盖运行时热路径和编译器流水线。

# 快速冒烟检查
dotnet run --project benchmark/Benchmark.csproj -c Release -- --smoke

# 写入简单 CSV 对比
dotnet run --project benchmark/Benchmark.csproj -c Release -- --compare

# 运行完整 BenchmarkDotNet 套件
dotnet run --project benchmark/Benchmark.csproj -c Release

使用上述命令运行冒烟检查、简单 CSV 对比或完整 BenchmarkDotNet 套件。

覆盖范围包括 domain 创建、空调用、函数调用、模块调用、闭包调用、对象、数组、HashMap、字符串、JSON、TDoc、Regex、CLR 互操作,以及词法分析器、解析器、发射器、单/多模块编译和 CompileBlock

TDoc 基准测试覆盖原始类型读写、普通嵌套对象读写、通用 ArrayInt32Array 和宽对象读取,以跟踪解析器热路径、普通对象写入和 packed array 分配回归。

dotnet run --project benchmark/Benchmark.csproj -c Release -- --filter "*TypedDocument*"

TDoc 热路径基线

以下结果来自 2026-08-20 的 Release/net10.0 BenchmarkDotNet ShortRun:Windows 11、Intel Xeon W-2235、.NET SDK 10.0.400 和 Runtime .NET 10.0.11。ShortRun 用于回归对比,而非跨机器的绝对 SLA 保证。

Benchmark Mean Allocated 说明
SerializePrimitive 226.0 ns 32 B 结果字符串分配;无中间树
DeserializePrimitive 246.7 ns 0 B 流式扫描与直接绑定
SerializeNestedObject 1.289 µs 296 B 规范文本与结果字符串
DeserializeNestedObject 2.410 µs 640 B 构造目标脚本对象
SerializePackedInt32Array1024 144.817 µs 20,360 B 文本输出是主要分配
DeserializePackedInt32Array1024 168.155 µs 4,192 B 目标 int[]/packed array
DeserializeGenericArray1024 178.815 µs 32,992 B 每元素通用 ScriptDatum 存储
DeserializeWideObject256 371.489 µs 16,608 B 目标对象上 256 个属性

该表记录平均耗时和管理内存分配。原始类型解析保持零分配;此处显示的对象和数组分配是返回的数据或输出文本,而非中间语法树。

解读 1M TDoc Map 测量

例如,对于约 1 MB 的 map.tdoc,报告如 AuroraTypedDocument.Deserialize | 38 ms | 1,000,136 KB 应视为端到端分配和吞吐量信号,而非仅解析器成本。主要成本通常是返回的对象图(每个字段一条属性项和值包装)、键/值字符串创建、字典增长、数值转换和垃圾回收工作。流式读取器避免中间语法树,但无法避免分配调用方要求物化的值。

要减少大型 map 的耗时和分配:

  • 保持读取器流式,并直接绑定到最终 ScriptObject/HashMap;不要先解析到临时通用树。
  • 字段数已知时预分配 map 大小,并在 API 允许时复用 serializer/reader 实例和临时缓冲区。
  • 对同构批量数据使用 packed array(Int32ArrayUInt8ArrayFloat64Array 及其他 typed array),而非通用 ScriptDatum 值数组。typeof 报告构造函数名;datum Kind 保持 Object
  • 避免重复子字符串/键复制;仅在宿主控制生命周期时对稳定 schema 键进行 intern 或池化。
  • 用原始类型、嵌套对象、宽对象、通用数组和 packed array 案例的基准测试分离解析器成本与物化成本。预热后报告 mean、p95 和管理内存分配。

具体收益取决于 map 的字段数、嵌套、值种类以及目标对象是否可复用。不要用单次 Stopwatch 运行跨机器对比;使用基准测试项目并保持输入和运行时固定。

强类型路径与分配

编译器将稳定的数值局部变量、Boolean 条件、整数循环变量和已知 packed array 引用保存在原生 CIL 表示中。详见 类型与强类型。模块边界热函数见 Native 函数

记录的热路径结果

以下结果来自 2026-08-15 的 Release/net10.0 BenchmarkDotNet ShortRun。它们是文档基线;不带 --filter 运行以重新生成完整套件。

dotnet run --project benchmark/Benchmark.csproj -c Release -- \
  --filter "*FunctionCallLoop*" "*ModuleCallLoop*" "*NumericLoop*"
Benchmark Size Mean Allocated 观察
NumericLoop 1,000 1.477 µs 0 B 数值局部变量与算术保持原生 CIL
NumericLoop 10,000 13.483 µs 0 B 线性时间增长,循环无分配
FunctionCallLoop 1,000 82.668 µs 0 B 同模块数值调用使用原生特化
FunctionCallLoop 10,000 766.803 µs 0 B 复用调用帧;无每调用上下文分配
ModuleCallLoop 1,000 122.591 µs 0 B 零分配跨模块通用调用边界
ModuleCallLoop 10,000 1.186 ms 0 B 受控动态分派成本,无每调用分配

该表记录文档热路径的 mean 和分配。六个案例在执行路径上均测得零管理内存分配。

环境为 BenchmarkDotNet 0.15.8、Windows 11 10.0.28000.2704、Intel Core i7-13700KF、.NET SDK 10.0.400、Runtime .NET 10.0.11ShortRun job。基准测试复用预构建的宿主参数数组,因此 Allocated 仅反映实际执行路径。

编写建议

  • 在热循环内缓存 length、模块常量和重复属性读取。
  • 对已知长度的数值工作使用 packed array 而非通用 Array
  • 对稳定 shape 使用 export type
  • 对模块边界热辅助函数使用 native func
  • 对大量文本构造使用 StringBuffer,而非循环中重复字符串拼接。
  • 仅在确实需要持久模块输出或调试符号时选择 Persistence;否则使用满足需求的最轻模式。
  • 优化前先测量,之后同时运行功能测试和基准测试。

后续步骤

Clone this wiki locally