- 编程语言
- AI Agent
- 编译器
- CLI
- 人工智能
【免费下载链接】baml
The programming language for agents
本文围绕 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; }逐段理解:
- 接口声明:
interface Shape { function area(self) -> int }声明一个仅含area方法的接口,self为隐式接收者。注意这里area是必需方法(没有默认实现体)。 - 实现类:
Square(字段side)与Rect(字段w、h)通过implements Shape { ... }块分别提供area的实现。两个类的area读取不同的字段、使用不同的公式,因此分派必须依据接收者的实际实现类,而不是共享布局。 - 多态构造:
pick(i: int) -> Shape根据i的奇偶交替返回Square或Rect——两者都装箱为接口类型Shape。这正是"多态"的来源:pick的返回类型是抽象接口,具体实现类在运行时才确定。 - 透过接口调用:
area_of(s: Shape) -> int { return s.area() }在一个静态类型为Shape的参数上直接调用方法,运行时必须解析s的真实实现类并定位其area方法体。 - 主循环:
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 直接相关的执行流水线为:
- 打包:
pack_baml调用baml-cli pack main --file <workload.baml> -o <out>把 BAML workload 打成独立可执行二进制(polymorphic_dispatch_100k.packed); - 期望值:运行打包后的二进制,取最后一行 stdout 作为期望输出;
- 跨语言校验:若环境中存在
python3/node/bun,分别运行python3 -S <py_file>、node <js_file>、bun <js_file>,任一输出与 BAML 期望值不一致,则在该行标记MISMATCH(结果表中显示(!))。这是保证三语言 workload 语义等价的硬性关卡; - 计时:默认走
time_command_adaptive——先丢弃 3 次预热,用预热中位数估算单次耗时,再按measurement_time反推采样数并夹在[min_samples=5, max_samples=100]之间;--runs指定时走time_command_fixed(固定次数)。统计输出中位数、标准差、样本数与 min/max; - 结果落盘:保存本轮数据到
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
相关推荐
BAML 接口 Match 分发基准深度解析:interfaces::match dispatch 100k 实测用例全解读
BAML 接口 Match 分发基准深度解析:interfaces::match dispatch 100k 实测用例全解读 BAML(the programm
编程语言AI Agent编译器CLI人工智能awesome-computer-vision 资源获取完整指南:一份清单搞定论文、数据集与工具库
awesome computer vision 资源获取完整指南:一份清单搞定论文、数据集与工具库 刚进入计算机视觉(CV)方向时,你大概率遇到过这种状况:论文
编程语言AI Agent编译器CLI人工智能微信批量发送终极指南:3分钟搞定1000条消息的免费神器
微信批量发送终极指南:3分钟搞定1000条消息的免费神器 你是否曾为给微信好友逐一发送相同消息而烦恼?无论是节日祝福、活动通知还是产品推广,手动发送不仅耗时耗力
编程语言AI Agent编译器CLI人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考