Nushell 性能基准实战:用 Tango 基准框架对比两个分支的实现性能
本文基于 Nushell 仓库的 benches/README.md 展开,讲解该项目如何围绕 tango 构建分支级性能对比能力:从 toolkit benchmark-compare 的完整工作流程,到 benchmarks.rs 中 160 余项基准测试的设计方式(glob、record/table、mut 赋值、par-each、插件编解码、解析器吞吐等),并给出可复制的 cargo bench 运行方法。读完你可以独立完成「当前分支 vs main」的性能回归分析,并理解每个基准对应的底层实现路径。
一、为什么 Nushell 用 tango 而不是单一 cargo bench
benches/README.md 开篇即说明了选型理由:Nushell 的性能基准基于 tango 构建——一个把两个版本的同一组基准「配对执行」的基准工具。其核心主张是:通过在同一时间窗口内交替运行 target 与 reference 两个二进制的同名基准,降低系统其他因素(CPU 频率漂移、后台负载等)带来的噪声,从而让两个实现的对比更可靠。
这与普通 cargo bench 的「单版本绝对耗时」定位不同:Nushell 关心的是 PR 级别的回归信号——「我这个分支相对 main 是变快还是变慢」,而不是某个绝对数值。仓库为此提供了两层入口:
- 脚本层:toolkit/benchmark.nu 里的
toolkit benchmark-compare/toolkit benchmark-log,一键完成「切分支 → 构建导出 → 配对运行 → 出报告」; - 代码层:benches/benchmarks.rs,用 tango 的
tango_benchmarks!宏声明了全部基准,用tango_main!()生成支持solo/compare子命令的 CLI。
二、快速上手:toolkit benchmark-compare
benches/README.md 给出的最短路径:
use toolkit.nu
# 对比当前分支与 main
toolkit benchmark-compare
# 或:对比指定的 target 修订与 reference 修订
toolkit benchmark-compare <target> <reference>
对照 toolkit/benchmark.nu 的源码,这条命令的完整工作流程如下:
- 确定分支:
reference缺省为main,target缺省为git branch --show-current拿到的当前分支; - 导出两个可执行产物:依次
git checkout到 target 与 reference,用cargo export <dir> -- bench把各自的基准二进制导出到./tango/bin/<branch>/benchmarks。注意注释中明确要求预先安装cargo-export(cargo-exporter 工具),这是脚本能够「不改 target 目录就产出两套二进制」的前提; - 切回原分支:
git checkout $current恢复工作区; - 配对运行并出报告:执行
^$target_bin compare $reference_bin -o -s 50 --dump ./tango/samples即由 target 二进制进入 tango 的compare模式,与 reference 二进制配对采样(-s 50表示采样轮次),结果 dump 到./tango/samples。
姊妹命令 toolkit benchmark-log 只基准当前(或指定)分支,用 compare -o -s 50 --dump ./tango/samples 把结果落盘到 ./tango/samples,适合给某条分支留存一份性能基线日志。
两者的共同前提是:所有产物都落在仓库根目录下的 ./tango 目录中(bin/<branch>/benchmarks 与 samples/),这也是 scripts/parser-bench-run.nu 等辅助脚本约定俗成的目录结构。
三、基准的注册结构:tango_benchmarks! 与 tango_main!
打开 benches/benchmarks.rs,文件末尾是一个 tango_benchmarks!( ... ) 宏调用,逐条列出全部基准的构造表达式,随后是 tango_main!()。这种组织方式带来三个实用特性(在 README 与源码注释中均有体现):
- 可单独运行:
cargo bench --bench benchmarks -- solo --filter '<glob>'可只跑名字匹配的一组基准,例如源码注释中给出的真实例子:NU_GLOB_BENCH_ROOT=/Users/fdncred/src/nushell cargo bench --bench benchmarks -- solo --filter '*recursive*' - 可配对比较:
compare子命令即toolkit benchmark-compare底层调用的模式; - 可过滤:
--filter支持 glob 匹配,scripts/parser-bench-run.nu 正是用--filter "parser_*"只跑解析器基准并把完整输出存到target/parser-bench/下的时间戳日志。
[[bench]] 配置位于 Cargo.toml(第 498-502 行附近):name = "benchmarks"、harness = false。harness = false 意味着 tango 自带命令行入口,不挂 libtest 的默认 benchmark harness,文件头注释也写明「跑全部基准用 cargo bench,跑单个基准用 cargo bench -- <regex>」。
四、基准怎么搭:引擎状态、命令级基准与夹具
所有「命令级」基准都通过两个公共原语搭建,这是理解整个套件的关键:
setup_engine()(benchmarks.rs#L33-L47):以nu_cmd_lang::create_default_context()为基座、经nu_command::add_shell_command_context装载完整 shell 命令集,并显式写入PWD环境变量(源码注释:parsing config.nu breaks without PWD set),最后generate_nu_constant()生成$nu常量。bench_command()(benchmarks.rs#L82-L114):每次迭代中克隆Stack与EngineState、engine.signals().reset()复位中断信号,然后用black_box(evaluate_commands(...))走与 REPL 相同的「nu命令求值」路径——所以这些基准测的是真实的 Nushell 脚本执行,而非手写的 API 调用。
几个值得注意的工程细节:
- glob 基准的仓库快照夹具:
GlobBenchFixture(benchmarks.rs#L136-L151)会把整个仓库(跳过.git与target)复制进tempfile临时目录,shared_glob_bench_fixture()用OnceLock保证进程内只复制一次;可用环境变量NU_GLOB_BENCH_ROOT指定被遍历的根目录。 - 实验选项的 RAII 守卫:
ExperimentalOptionGuard::set_dc_glob(bool)(benchmarks.rs#L116-L134)用 unsafe 临时改写进程级DC_GLOB(定义于 crates/nu-experimental/src/options/dc_glob.rs),Drop时自动还原,使 legacy 与 dc-glob 两种实现能在同一进程中安全交替基准。
五、基准清单逐类解读
tango_benchmarks! 注册了近 160 个基准,按主题可归为以下几类,每类都对应源码里明确标注的优化点或回归面:
1. 标准库加载
load_standard_lib:直接调nu_std::load_standard_library;load_use_standard_lib:在装载了nu_cmd_extra额外命令(标准库依赖format number等)的引擎上执行use std,测量「通用use加载全部模块」的开销。
2. 递归 glob / ls(legacy vs 新实现)
ls_recursive_legacy / ls_recursive_dc 与 glob_recursive_legacy_wax / glob_recursive_dc_glob 成对出现,命令分别为 ls <root>/**/* | length | ignore 与 glob '**/*' | length | ignore,由 dc_glob_enabled 布尔值切换实验开关。这正是 tango「配对比较两个实现」的典型用例:同一个命令、两种 glob 引擎,靠 compare 直接读出差异。
3. 类型加宽(type widening)
type_widen_simple、type_widen_large_records(50 字段 Record 求 union)、type_widen_large_oneof(32 个 Record 的 one_of)、type_widen_chain(链式 100 次 union 后的再 union),压测 nu-protocol 类型系统 中 Type::union 的合并逻辑。
4. 解析器四阶段 × 四种语料
这是套件中信息密度最高的一组(benchmarks.rs#L1178-L1391)。基准名内嵌字节/字符数(如 parser_lex_small_127b_125c),源码注释解释:这种命名便于自动推导吞吐指标(bytes/sec、chars/sec),用于 PR 描述与性能追踪。
| 阶段 | 测量函数 | 隔离目标 |
|---|---|---|
lex |
lex | 纯分词,不含解析 |
lite |
lite_parse | 词法单元 → LiteBlock(管道结构),无 AST |
parse_block |
parse_block | 预分词 tokens → AST(Block),不含编译 |
parse |
parse 全流程 |
字节 → 编译后的 Block,含顶层块 eager IR 编译 |
语料分四档:small(约 127 字节合成短管道,噪声小)、medium(create_parser_pipeline_script(80) 生成的 80 条管道,规模可预测)、large(递归收集 crates/nu-std/std 全部 .nu 文件并排序拼接,覆盖真实标准库代码)、real_world(include_str! 引入的 toolkit/mod.nu)。
配套的 scripts/parser-bench-run.nu 与 scripts/parser-bench-throughput.nu 负责把 --filter "parser_*" 的运行输出持久化、解析吞吐,形成从基准到报告的完整工具链。
5. 数据类型:binary / list / record / table
- binary:
binary_value_clone_2097152b(2 MiB 克隆)、binary_slice_into_int_2097152b_100_reads(自定义bench-u16命令做切片 + 大端into int,共 100 次读)、binary_skip_shared_2097152b_half_100_reads(每次skip一半字节,注释说明原值保留在 stack 上以验证共享后端存储); - list:
list_value_clone_100000、stack_list_get_var_100000(栈上 10 万元素的变量读取); - record:
record_create_{1,10,100,1000}(let record = { col_i: i })、record_flat_access_*($record.col_0)、record_nested_access_{1..128}(嵌套深度翻倍的.col.col...访问)、record_insert_{n}_{m}(链式insert); - table:
table_create/get/select/insert_row/insert_col各按规模矩阵展开,数据由create_example_table_nrows生成[[foo bar baz]; ...]字面量。
6. 字符串替换
str_replace_regex_list_1000(对 1000 个字符串做 str replace -a -r '\d+' 'N')、str_replace_regex_table_500_2cols(指定列的表替换)、str_replace_multiline_list_1000(--multiline 模式)。
7. 求值与并发:eval / for / each / par-each
eval_interleave_{100,1000,10000}及其_with_interrupt变体:seq 1 n | wrap a | interleave { seq 1 n | wrap b };带中断变体通过engine.set_signals(Signals::new(...))装上信号源,验证流式中断检查路径的开销;eval_for / eval_each / eval_par_each各按n = {1,10,100,1000,10000}展开,命令形如(for $x in (1..n) { 1 })、(1..n) | each {|_| 1}、(1..n) | par-each -t 2 {|_| 1};par-each线程池三组对照(源码注释直接点明动机):par_each_default_pool_{n}走全局池、单次大调用;par_each_many_calls_{10,50,100}串行发起 10/50/100 次小par-each,注释写明「main 分支每次调用都要新建私有池」,这是最强的池复用回归信号;par_each_many_calls_threads_*则带显式-t 2,测缓存的自定义池路径;eval_default_config/eval_default_env:通过ConfigFileKind(来自 nu-config)取默认配置文件字节,用eval_source走真实配置加载路径。
8. mut 赋值优化:端到端 + 栈级 + 值级三层对照
这是最能体现「基准服务于优化」设计思想的一组,注释把新旧路径写得很清楚:
- 端到端(IR
UpdateVarCellPath路径):mut_record_assign_{1k,10k,100k}:for _ in 1..1000 { $r.a = 'x' },setup 只建一次大 record,计时体内只有赋值,让赋值成本主导而非引擎克隆开销;mut_list_assign_*($l.0 = {a: 999},与 record 走同一 IR 路径)、mut_record_compound_assign_*($r.a += 1,payload 很大,clone-on-write 回归会暴露);mut_record_update_*是慢基线:$r = ($r | update a { 'x' })整值替换的管道路径,用于量化mut_record_assign_*的优化收益;
- 栈级:
stack_upsert_clone_{n}模拟旧路径(get_var克隆 → upsert 副本 →add_var写回,强制 SharedCow 深拷贝);stack_upsert_inplace_{n}走新路径upsert_var_cell_path(唯一所有权原地更新); - 值级:
upsert_record_clone / upsert_record_inplace / upsert_list_clone / upsert_list_inplace,分别在共享克隆与唯一所有权两种所有权状态下测upsert_data_at_cell_path的纯变更成本; - concat:
value_concat_general / empty_lhs / empty_rhs测纯Value::concat(含[] ++ xs、xs ++ []两侧空短路);concat_prebuilt_*则在栈上预建列表后用for _ in 1..100 { $a ++ $b | ignore }放大 100 次以压过引擎克隆噪声,并有命令级对应物。
9. 插件编解码
encode_json / encode_msgpack / decode_json / decode_msgpack 以 (100, 5) 与 (10000, 15) 两个行列规模展开:数据是 PluginOutput::CallResponse(PluginCallResponse::value(...)) 包裹的表格值,经 nu-plugin-core 的 EncodingType(json / msgpack)编码;decode 基准先把 payload 编码一次,再在迭代中对 Cursor 复位后解码,隔离序列化器本身的成本。
10. 表格渲染
create_nu_table(rows, cols) 构造带表头的 NuTable,set_structure(false, true, false) 配置边框后:
table_render_{10,100,1000}x5、table_render_100x10:默认主题、200 列宽draw(200);table_render_*_rounded:TableTheme::rounded()圆角主题;table_render_10x{20,50}_wide:宽表场景draw(1000)。
六、无编译环境的轻量替代:mut_assign_bench.nu
benches/mut_assign_bench.nu 是一组「手动冒烟基准」,用标准库 std/bench 的 bench --rounds 50 --warmup 5 --pretty 直接跑 Nushell:
nu benches/mut_assign_bench.nu
它复用与 Rust 版相同的场景($r.a = 'x'、$l.0 = {a: 999}、管道 update 基线、$r.a += 1、三种 concat、50 次小 par-each),文件头注释给出了分工建议:快速冒烟用它,分支对 main 的正式对比仍应走 tango:
cargo bench --bench benchmarks -- solo --filter 'mut_record_assign|stack_upsert|par_each_many|value_concat'
注释还点出一个 Nushell 语言层面的限制:std/bench 闭包无法捕获外层的 mut 变量,因此每个基准都在闭包内部声明 mut(setup 一并计入计时)。
七、可复制的运行方式小结
结合 README 与源码注释,常用命令汇总(均在仓库根目录执行,且 cargo-export 需已安装):
# 全量配对比较(脚本层入口)
nu -c 'use toolkit.nu; toolkit benchmark-compare'
nu -c 'use toolkit.nu; toolkit benchmark-compare my-feature main'
nu -c 'use toolkit.nu; toolkit benchmark-log'
# 单跑一组基准(tango solo 模式,glob 过滤)
cargo bench --bench benchmarks -- solo --filter '*recursive*'
cargo bench --bench benchmarks -- solo --filter 'parser_*'
# 指定 glob 基准的遍历根目录
NU_GLOB_BENCH_ROOT=/path/to/target cargo bench --bench benchmarks -- solo --filter '*recursive*'
# 解析器基准 + 日志持久化
nu scripts/parser-bench-run.nu
结果与样本统一落在 ./tango 目录:bin/<branch>/benchmarks 为导出的双分支基准二进制,samples/ 为 --dump 的采样原始数据,可离线复看对比结果。
八、小结
Nushell 的 benches 目录展示了一套完整的开源项目性能工程实践:以 benches/README.md 描述的 tango 配对比较为核心,toolkit/benchmark.nu 把「切分支 → cargo export → compare -s 50 → 样本落盘」收敛为一条 toolkit benchmark-compare;benches/benchmarks.rs 则用统一脚手架(setup_engine + bench_command + 实验选项 RAII 守卫 + 仓库快照夹具)覆盖了从 lexer 四阶段吞吐、glob 双实现、mut 赋值新旧路径、par-each 线程池复用,到插件编解码与表格渲染的回归面。对贡献者而言,任何触及解析、求值、类型系统或数据结构的改动,都可以用 solo --filter 精准定位受影响基准,再用 compare 模式在 PR 中给出可信的性能证据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00