☰
BAML 接口多态分派基准测试:polymorphic-dispatch-100k workload 深度解析
2026/9/25 2:53:07 网站建设 项目流程
  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

本文围绕 BAML 仓库内置基准套件(tools/speedtest)中的interfaces::polymorphic dispatch 100kworkload 展开,完整解读其 BAML/Python/TypeScript 三语言对照实现、接口多态调度的底层原理,并给出该 workload 在本地基准工具链中的运行、验证与剖析方法。读完本文,你将掌握如何读懂并运行这类接口分派型性能用例,并能结合源码理解 BAML 运行时"开放式世界"方法分派的设计取舍。

一、workload 在基准套件中的定位

在 baml_language/tools/speedtest/workloads 目录下,基准用例按主题分目录组织:compute/(纯计算)、classes/(类与方法调用)、interfaces/(接口相关)、concurrency/(并发)、string/(字符串)。本篇文章所讨论的 polymorphic-dispatch-100k.md 属于interfaces/类别,聚焦一个核心问题:

当程序通过接口类型(existential 接收者)调用方法时,运行时如何把调用分派到具体实现类的方法上,以及这种分派在 10 万次循环中的成本是多少。

同目录下的三个兄弟 workload 从不同角度度量接口机制:

  • match-dispatch-100k.md:通过match模式匹配实现"手动分派"(let d: Dog => ...);
  • field-access-100k.md:度量接口字段读取(该文档明确指出,接口方法分派本来就是开放式世界的,而字段读取路径曾由编译期 implementor 开关改为virtual_load_field,因此字段访问才是成本迁移最明显的路径);
  • 本文主角 polymorphic-dispatch-100k.md:度量接口方法的多态分派,接收者每次交替为两种不同实现类。

二、BAML workload 全文与逐段解读

原文档的 BAML 部分定义了一个完整的"形状面积"程序,使用interface、implements、for循环与多次方法分派:

interface Shape { function area(self) -> int } class Square { side: int implements Shape { function area(self) -> int { return self.side * self.side } } } class Rect { w: int h: int implements Shape { function area(self) -> int { return self.w * self.h } } } function pick(i: int) -> Shape { if i % 2 == 0 { return Square { side: i }; }; return Rect { w: i, h: 3 }; } function area_of(s: Shape) -> int { return s.area() } function main() -> int { let acc = 0; for (let i = 0; i < 100000; i += 1) { acc += area_of(pick(i)); }; return acc; }

逐段理解:

  1. 接口声明:interface Shape { function area(self) -> int }声明一个仅含area方法的接口,self为隐式接收者。注意这里area是必需方法(没有默认实现体)。
  2. 实现类:Square(字段side)与Rect(字段w、h)通过implements Shape { ... }块分别提供area的实现。两个类的area读取不同的字段、使用不同的公式,因此分派必须依据接收者的实际实现类,而不是共享布局。
  3. 多态构造:pick(i: int) -> Shape根据i的奇偶交替返回Square或Rect——两者都装箱为接口类型Shape。这正是"多态"的来源:pick的返回类型是抽象接口,具体实现类在运行时才确定。
  4. 透过接口调用:area_of(s: Shape) -> int { return s.area() }在一个静态类型为Shape的参数上直接调用方法,运行时必须解析s的真实实现类并定位其area方法体。
  5. 主循环:main以 10 万次迭代累加area_of(pick(i))的结果。每次迭代包含:一次多态构造 + 一次接口方法分派 + 一次整数乘法/加法,构成稳定的分派压力。

在 10 万次迭代中,area的实际调用会以约 1:1 的比例交替落在Square与Rect上,且两种实现的字段布局不同(side与w/h不在同一槽位),因此每次调用都必须真正依赖接收者的实现信息完成分派,不存在共享布局下的侥幸优化空间。

三、跨语言对照:Python 与 TypeScript 的等价实现

原文档为同一 workload 提供了 Python 与 TypeScript 版本,二者与本仓库基准工具的设计直接相关——runner.py会用它们做跨语言输出一致性校验(见下文第四节)。

Python 对照实现:

class Square: __slots__ = ('side',) def __init__(self, side): self.side = side def area(self): return self.side * self.side class Rect: __slots__ = ('w', 'h') def __init__(self, w, h): self.w = w; self.h = h def area(self): return self.w * self.h def pick(i): return Square(i) if i % 2 == 0 else Rect(i, 3) acc = 0 for i in range(100000): acc += pick(i).area() print(acc)

要点:__slots__显式限定字段以贴近紧凑布局;pick用三元表达式交替构造;主循环等价于 BAML 版本,最终print(acc)输出结果。

TypeScript 对照实现:

class Square{constructor(side){this.side=side}area(){return this.side*this.side}} class Rect{constructor(w,h){this.w=w;this.h=h}area(){return this.w*this.h}} function pick(i){return i%2===0?new Square(i):new Rect(i,3)} let acc=0;for(let i=0;i<100000;i++){acc+=pick(i).area()}console.log(acc)

三份代码保持相同的语义与循环结构,输出一致(都是同一整数累加结果),这是基准可比性与正确性的基础。

四、workload 的加载机制:Markdown 即基准定义

tools/speedtest采用"Markdown 即 workload 定义"的设计。加载器 loader.py 解析每个.md文件:

  • 第一行#标题作为 workload 全名(例如interfaces::polymorphic dispatch 100k),其父目录名作为 category(interfaces);
  • 正则提取## BAML、## Python、## Typescript三个代码块(parse_workload_md);
  • 三个代码块任一缺失则整个 workload 被跳过(if not baml or not python or not js: return None);
  • 目录以排序后的rglob("*.md")扫描,形成有序 workload 列表。

loader.py还预留了## eval-setup小节(可选),用于执行一段 Python 设置代码,并通过$$var模板(_DDTemplate,单$为字面量)对三份源码做变量替换——本 workload 未使用该机制,但它是同套件中复用型 workload 的扩展点。

此外,export_baml.py 将全部 workload 的展开后BAML 源码以 JSON 数组({"name", "category", "baml"})输出到 stdout,其文件头注释明确说明:这是让 Rust 基准套件(crates/baml_tests)复用这些 workload 作为 CodSpeed 基准的桥接层,避免重复实现.md解析与模板逻辑。也就是说,这份polymorphic-dispatch-100k.md不仅服务于 Python 侧计时,还被导出供 Rust 侧持续基准复用。

五、如何运行与验证该 workload

基准入口是 cli.py 定义的speedtest命令(子命令run/compare/open/list/baselines,未给子命令时默认视为run)。只运行本文 workload 的典型方式:

speedtest run --filter "polymorphic" --build

核心命令行参数(依据 cli.py):

参数作用默认值
--build先cargo build --release -p baml_pack_host -p baml_cli再基准关闭
--filter只跑名称包含该子串的 workload(可重复,大小写不敏感)空(跑全部)
--only-baml跳过 python/node/bun 计时关闭
--runs N固定每 workload 跑 N 次(覆盖自适应计时)None(自适应)
--measurement-time SECS自适应计时下每 workload 的目标测量秒数5.0
--baml PATH指定 baml-cli 路径(默认target/release/baml-cli)自动推导
--tag NAME为本轮结果打标签(类似 git tag)无
--profile用samply record --save-only采集 CPU profile(要求 PATH 中有 samply)关闭
--profile-baml PATH指定 profiling 构建的 baml-cli由 release 路径推导
--results-dir结果保存目录~/.speedtest/

runner.py中与本文 workload 直接相关的执行流水线为:

  1. 打包:pack_baml调用baml-cli pack main --file <workload.baml> -o <out>把 BAML workload 打成独立可执行二进制(polymorphic_dispatch_100k.packed);
  2. 期望值:运行打包后的二进制,取最后一行 stdout 作为期望输出;
  3. 跨语言校验:若环境中存在python3/node/bun,分别运行python3 -S <py_file>、node <js_file>、bun <js_file>,任一输出与 BAML 期望值不一致,则在该行标记MISMATCH(结果表中显示(!))。这是保证三语言 workload 语义等价的硬性关卡;
  4. 计时:默认走time_command_adaptive——先丢弃 3 次预热,用预热中位数估算单次耗时,再按measurement_time反推采样数并夹在[min_samples=5, max_samples=100]之间;--runs指定时走time_command_fixed(固定次数)。统计输出中位数、标准差、样本数与 min/max;
  5. 结果落盘:保存本轮数据到results_dir,并写入baselines/<branch>/latest(可--tag打标),随后可用speedtest compare <branch-a> <branch-b>对比基线、speedtest open打开浏览器 UI。

若--profile开启,还会以samply record --save-only -o <slug>.json <profiling-baml-cli> run --file <baml_file> -f main -- main的形式采集 profile 文件并随结果保存,可用于定位分派热点。

六、接口分派的底层实现视角

从仓库源码可以进一步印证该 workload 所压测的运行时机制。

1. 开放式世界的 impl 表烘焙。bex_vm的 interface_registry.rs(白盒测试)断言:打包进Program::packages的 impl 规则,其方法表是PROVIDED-ONLY——只包含 impl 块显式声明的方法,绝不烘焙一份接口默认方法的拷贝;默认方法由分派时通过接口对象的default_fn现场采用。对本文 workload 而言,Square implements Shape与Rect implements Shape各自的方法表恰好只含area,分派只需在接收者的实现规则中查到area -> area的方法体 FQN 即可。

2. 分派必须依赖接收者。field-access-100k.md的注释提供了关键背景:接口方法分派本就是开放式世界的;而接口字段读取曾依赖编译期 implementor 开关,后来改为virtual_load_field。从源码结构看,polymorphic-dispatch-100k正是用来持续盯住"方法分派"这条路径的成本——两个实现类的area读取不同槽位的字段,说明每次调用都必须走接收者相关的解析,无法退化为共享布局直读。

3. 与 match 手动分派的对照。同目录 match-dispatch-100k.md 使用match (a) { let d: Dog => d.noise() ... }显式按类型分支,等价于 Python 的isinstance链与 TypeScript 的instanceof链。将两个 workload 的耗时并排对比,即可看出"运行时多态分派"与"源码级手动分派"在 BAML 运行时中的相对成本,这正是speedtest compare用武之地。

七、扩展与延伸阅读

  • 阅读同目录 field-access-100k.md,了解接口字段读取(virtual_load_field)这一成本迁移路径的专项度量;
  • 阅读 match-dispatch-100k.md,对比 match 手动分派;
  • 想要把本 workload 加入 Rust 侧 CodSpeed 基准,可复用 export_baml.py 的 JSON 导出;
  • 想要复现完整基准流程,从 runner.py 与 cli.py 入手,配合--build构建baml-cli与baml_pack_host,即可在本地跑通打包、校验、计时、对比的完整闭环。

适用前提说明:以上运行方式与参数以当前仓库baml_language/tools/speedtest的实际实现为准;本地运行需要 Rust 工具链(构建baml-cli/baml_pack_host)、可选的python3/node/bun(用于跨语言校验)以及可选的samply(用于--profile)。本文不包含任何具体性能数字,基准结果应在本机环境实测获得。

  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

相关推荐

上一篇:PaddleSpeech Conformer 训练性能基准测试实践:AISHELL-1 Benchmark 脚本、运行流程与日志解析
下一篇:5分钟上手浏览器远程桌面:mstsc.js终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询