首页
/ Nushell 性能基准实战:用 Tango 基准框架对比两个分支的实现性能

Nushell 性能基准实战:用 Tango 基准框架对比两个分支的实现性能

2026-09-05 17:13:42作者:蔡怀权

本文基于 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 的源码,这条命令的完整工作流程如下:

  1. 确定分支reference 缺省为 maintarget 缺省为 git branch --show-current 拿到的当前分支;
  2. 导出两个可执行产物:依次 git checkout 到 target 与 reference,用 cargo export <dir> -- bench 把各自的基准二进制导出到 ./tango/bin/<branch>/benchmarks。注意注释中明确要求预先安装 cargo-export(cargo-exporter 工具),这是脚本能够「不改 target 目录就产出两套二进制」的前提;
  3. 切回原分支git checkout $current 恢复工作区;
  4. 配对运行并出报告:执行 ^$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>/benchmarkssamples/),这也是 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 = falseharness = 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):每次迭代中克隆 StackEngineStateengine.signals().reset() 复位中断信号,然后用 black_box(evaluate_commands(...)) 走与 REPL 相同的「nu 命令求值」路径——所以这些基准测的是真实的 Nushell 脚本执行,而非手写的 API 调用。

几个值得注意的工程细节:

  • glob 基准的仓库快照夹具GlobBenchFixturebenchmarks.rs#L136-L151)会把整个仓库(跳过 .gittarget)复制进 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_dcglob_recursive_legacy_wax / glob_recursive_dc_glob 成对出现,命令分别为 ls <root>/**/* | length | ignoreglob '**/*' | length | ignore,由 dc_glob_enabled 布尔值切换实验开关。这正是 tango「配对比较两个实现」的典型用例:同一个命令、两种 glob 引擎,靠 compare 直接读出差异。

3. 类型加宽(type widening)

type_widen_simpletype_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 字节合成短管道,噪声小)、mediumcreate_parser_pipeline_script(80) 生成的 80 条管道,规模可预测)、large(递归收集 crates/nu-std/std 全部 .nu 文件并排序拼接,覆盖真实标准库代码)、real_worldinclude_str! 引入的 toolkit/mod.nu)。

配套的 scripts/parser-bench-run.nuscripts/parser-bench-throughput.nu 负责把 --filter "parser_*" 的运行输出持久化、解析吞吐,形成从基准到报告的完整工具链。

5. 数据类型:binary / list / record / table

  • binarybinary_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 上以验证共享后端存储);
  • listlist_value_clone_100000stack_list_get_var_100000(栈上 10 万元素的变量读取);
  • recordrecord_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);
  • tabletable_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 的纯变更成本;
  • concatvalue_concat_general / empty_lhs / empty_rhs 测纯 Value::concat(含 [] ++ xsxs ++ [] 两侧空短路);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-coreEncodingTypejson / msgpack)编码;decode 基准先把 payload 编码一次,再在迭代中对 Cursor 复位后解码,隔离序列化器本身的成本。

10. 表格渲染

create_nu_table(rows, cols) 构造带表头的 NuTableset_structure(false, true, false) 配置边框后:

  • table_render_{10,100,1000}x5table_render_100x10:默认主题、200 列宽 draw(200)
  • table_render_*_roundedTableTheme::rounded() 圆角主题;
  • table_render_10x{20,50}_wide:宽表场景 draw(1000)

六、无编译环境的轻量替代:mut_assign_bench.nu

benches/mut_assign_bench.nu 是一组「手动冒烟基准」,用标准库 std/benchbench --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 exportcompare -s 50 → 样本落盘」收敛为一条 toolkit benchmark-comparebenches/benchmarks.rs 则用统一脚手架(setup_engine + bench_command + 实验选项 RAII 守卫 + 仓库快照夹具)覆盖了从 lexer 四阶段吞吐、glob 双实现、mut 赋值新旧路径、par-each 线程池复用,到插件编解码与表格渲染的回归面。对贡献者而言,任何触及解析、求值、类型系统或数据结构的改动,都可以用 solo --filter 精准定位受影响基准,再用 compare 模式在 PR 中给出可信的性能证据。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384