首页
/ uv 基准测试实战:BENCHMARKS.md 四项核心性能场景的复现与源码剖析

uv 基准测试实战:BENCHMARKS.md 四项核心性能场景的复现与源码剖析

2026-09-03 15:33:21作者:彭桢灵Jeremy

本文以仓库根目录的 BENCHMARKS.md 为核心,完整讲解 uv 官方基准测试的四个场景(Warm/Cold × Install/Resolve)的定义、测试前提与限制,并基于 scripts/benchmark 工具包与 uv-dev 的渲染代码,给出从构建二进制、运行 hyperfine 到生成柱状图的完整可复现流程。读完本文,你可以独立在本地复现 uv 对比 pip-tools、Poetry、PDM 的解析与安装基准,并理解 “warm/cold” 在实现层究竟由哪些清理命令保证。

uv 热缓存安装基准对比图:各工具执行 uv sync 类安装操作(热缓存)的耗时柱状对比 uv 冷缓存解析基准对比图:各工具执行 uv lock 类依赖解析(冷缓存)的耗时柱状对比

一、基准测试的前提条件与适用边界

BENCHMARKS.md 开篇即给出两条必须理解的前提:官方数据全部在 macOS 上、使用 Python 3.12.4(针对非 uv 工具)测得,且性能会因环境与负载不同而剧烈变化。

1.1 操作系统与文件系统会影响安装类基准

文档明确指出:不同操作系统与文件系统的基准性能差异可能非常大,因为 uv 会根据底层文件系统的能力选择不同的安装策略。文档举出的例子是:uv 在 macOS 上使用 reflinking(反射链接),在 Linux 上使用 hardlinking(硬链接)。这意味着把 macOS 测得的 “安装” 数据直接外推到 Linux 是不严谨的,安装类基准的可比性只在同平台内部成立。

1.2 依赖集本身会掩盖工具差异

第二条前提是:基准性能也高度依赖被安装的包集合。文档特别说明:如果一个解析过程需要构建单个计算密集型的源码分发包(sdist),那么各工具之间的表现会趋于接近——因为此时瓶颈(构建耗时)与使用哪个工具无关。这提醒读者:任何单一工作负载下的对比都不能代表工具在所有场景下的相对水平。

1.3 工作负载:Trio 的 docs-requirements.in

官方基准选用 Trio 项目的 docs-requirements.in 作为“真实世界项目”的代表样本。该文件在仓库中的拷贝是 test/requirements/trio.in,第一行注释即声明其来源是 Trio 仓库的 docs-requirements.in;内容包含 sphinx、jinja2、sphinx_rtd_theme、towncrier 等文档构建依赖,以及 cffi、attrs、pyOpenSSL 等 Trio 自身依赖,是一个带有版本约束(如 sphinx >= 4.0, < 6.2)和平台标记(如 cffi; os_name == "nt")的真实依赖集。对应的预编译结果文件为 test/requirements/compiled/trio.txt,用于安装类基准。

二、四个基准场景的准确定义

BENCHMARKS.md 将基准划分为四个小节,全部遵循“柱状越低越好”(smaller bar is better)的读数规则:

场景 对应 uv 命令 缓存状态 等价语义
Warm Installation(热安装) uv sync 热缓存 删除并重建虚拟环境后,装入同一台机器上之前安装过的依赖
Cold Installation(冷安装) uv sync 冷缓存 等价于在新机器或 CI 上跑 uv sync(假设包管理器缓存不在多次运行间共享)
Warm Resolution(热解析) uv lock 热缓存、无已有 lockfile 等价于删掉现有 requirements.txt 后从 requirements.in 重新生成
Cold Resolution(冷解析) uv lock 冷缓存 等价于在新机器或 CI 上跑 uv lock(缓存不跨运行共享)

其中 “warm” 与 “cold” 的分界点就是每次计时运行前缓存目录是否被清空,这一点在基准脚本源码中有明确实现(见第四节)。四张官方图表在仓库内的本地拷贝分别位于 install-warm.pnginstall-cold.pngresolve-warm.pngresolve-cold.png,可随时对照阅读。

三、复现官方基准:scripts/benchmark 工具包

BENCHMARKS.md 的 “Reproduction” 一节说明:所有官方基准都由 scripts/benchmark生成,该包封装了 hyperfine 工具,用来把 uv 与多种其他打包工具放进同一计时框架对比。

3.1 前置条件

按文档列出的三项要求:

  1. 一个本地的 uv release 构建:cargo build --release
  2. PATH 中已安装生产版 uv 二进制;
  3. 系统已安装 hyperfine 命令行工具。

从源码可以印证第 1 条:resolver.pyUvProject 在未显式指定路径时,默认使用的正是仓库根的 target/release/uv 构建产物。此外,scripts/benchmark/pyproject.toml 中通过 exclude-newer = "P7D" 与固定版本约束(如 uv-build==0.12.3 构建约束)冻结了基准环境的依赖状态,保证工具链版本可复现。该包的入口声明为 pyproject.toml 中的 resolvertools 两个脚本。

3.2 解析类基准命令(对比 pip-compile、Poetry、PDM)

文档给出的原始命令(需在 scripts/benchmark 目录下运行):

uv run resolver \
    --uv-project \
    --poetry \
    --pdm \
    --pip-compile \
    --benchmark resolve-warm --benchmark resolve-cold \
    --json \
    ../requirements/trio.in

3.3 安装类基准命令(对比 pip-sync、Poetry、PDM)

uv run resolver \
    --uv-project \
    --poetry \
    --pdm \
    --pip-sync \
    --benchmark install-warm --benchmark install-cold \
    --json \
    ../requirements/compiled/trio.txt

两条命令都应从 scripts/benchmark 目录执行。需要注意一处仓库布局变化:从当前源码结构看,trio.incompiled/trio.txt 实际位于 test/requirements/ 目录下,文档中的 ../requirements/ 相对路径反映的是历史目录结构,复现时请把最后的文件参数调整为本机实际路径(例如 ../../test/requirements/trio.in)。

3.4 参数语义(结合源码补充)

resolver.py 的 main() 定义了完整的参数集,除文档示例中的 --json--benchmark 与各工具开关外,还有几个对结果稳定性很关键的参数:

参数 默认值 说明
--python 3.12.3 基准使用的 Python 版本(见 resolver.py#L1284-L1283
--warmup 3 预热运行次数,不计入统计
--min-runs 10(与 --runs 均未指定时) 每个命令的最小计时运行次数
--runs 固定运行次数,与 --min-runs 互斥
--benchmark / -b 按扩展名推断 可多次指定;不指定时 .in 文件推断为 resolve-cold+resolve-warm.txt 文件推断为 install-cold+install-warm(见 resolver.py#L1436-L1444
--uv-project / --uv-pip / --poetry / --pdm / --pip-compile / --pip-sync 关闭 选择参与对比的工具;全部不指定则六者全跑
--<tool>-path 追加同工具的多个二进制路径,用于多版本 uv 回归对比
--json 关闭 导出 <benchmark>.json,供渲染图表使用

四、源码剖析:warm 与 cold 在实现层的保证

官方文档对 warm/cold 的描述是语义级的,而 resolver.py 给出了严格的实现定义。

4.1 实际存在六个场景,文档展示其中四个

Benchmark 枚举 定义了六个场景:resolve-coldresolve-warmresolve-incrementalresolve-noopinstall-coldinstall-warm。BENCHMARKS.md 呈现的是前两者与后两者的安装/解析四个;而 resolve-incrementalresolve-noop 主要用于 uv 多版本之间的回归对比——incremental 场景会在已有 lockfile 基础上向依赖集中注入一个“理想情况下兼容所有文件但不出现在任何解析结果中”的新依赖(源码中该依赖被硬编码为 django,见 resolver.py#L80),noop 则直接对未变更的 lockfile 重新解析。

4.2 “冷”与“热”由 prepare 命令逐次执行保证

每个工具场景为每个基准生成一个 Command(name, prepare, command) 三元组,其中 preparehyperfine 在每次计时运行前执行 的清理命令。以 uv 项目接口(UvProject)为例:

  • resolve_cold 的 prepare 是 rm -rf .cache && rm -f uv.lock(见 resolver.py#L1059-L1078)——既清缓存又删 lockfile;
  • resolve_warm 的 prepare 只有 rm -f uv.lock——保留缓存,模拟“有本机缓存但需重新生成锁文件”;
  • install_warm 的 prepare 是 virtualenv --clear ...(见 resolver.py#L1225-L1258)——重建虚拟环境但不清缓存,对应文档“删除并重建 venv、装入装过的包”的语义;
  • 冷安装则在 prepare 中追加 rm -rf .cache

其他工具也各有对应的缓存目录清理逻辑,例如 Poetry 场景通过 POETRY_CONFIG_DIR/POETRY_CACHE_DIR/POETRY_DATA_DIR 环境变量把缓存重定向到临时工作目录(resolver.py#L390-L415),PDM 则通过 pdm config cache_dir 达成同样效果。所有工具都在 main() 末尾被放进 tempfile.TemporaryDirectory() 的独立子目录中运行,彼此隔离。

4.3 hyperfine 命令的拼装

Hyperfine 封装类 把上述配置翻译为一条 hyperfine 调用:--export-json <name>.json(当 --json 打开时)、--warmup N--min-runs N--runs N、逐命令的 --command-name--prepare,最后拼接各命令本体。这就是 --json 产出的 <benchmark>.json 文件的来源。

补充一点:tools.py 提供了姊妹入口 uv run tools,用同样方式对比 uv tool install/runpipx install(默认被测工具为 flask,见 tools.py#L18),并特意保留 pipx 的 shared 共享环境、只清理已安装工具目录,以区分“机器上首次调用 pipx”与“冷缓存 pipx run”两种语义(tools.py#L80-L84 注释)。

五、生成对比图表:uv-dev 的 render-benchmarks

文档指出,基准脚本运行后可用以下命令生成图表(四个场景各一条):

cargo run -p uv-dev --all-features render-benchmarks resolve-warm.json --title "Warm Resolution"
cargo run -p uv-dev --all-features render-benchmarks resolve-cold.json --title "Cold Resolution"
cargo run -p uv-dev --all-features render-benchmarks install-warm.json --title "Warm Installation"
cargo run -p uv-dev --all-features render-benchmarks install-cold.json --title "Cold Installation"

并特别提醒:若生成的图中缺少标签文字,需要安装 Roboto 字体

render_benchmarks.rs 解释了这两点:

  • 该模块声明为 #![cfg(feature = "render")]第 1 行),因此必须带 --all-features 才能编译进二进制;
  • 它读取 hyperfine 的 JSON 结果,用 poloto 以每个命令的 mean(均值)为条高生成柱状图(Y 轴单位是秒,与“越低越好”一致),再用 resvg 把 SVG 按 1600px 目标宽度缩放渲染为 PNG,输出路径为输入 JSON 同名的 .png 文件;
  • 字体通过 load_fonts()系统字体库加载(render_benchmarks.rs#L107-L118),字体库中没有 Roboto 时标签自然渲染不出来——这正是文档要求安装 Roboto 的原因。

六、冷基准波动(Flaky Benchmarks)的排查

BENCHMARKS.md 的 Troubleshooting 一节给出了一个高价值经验:如果冷基准出现高方差,大概率是撞上了 ISP 的限流或 DDoS 防护——ISP 会以 TCP reset 强制断开连接。文档的判断依据是:这些基准会在极短时间内发出完全相同的请求(对并发度极高的 uv 尤其如此),恰好触发过滤规则。文档建议的 workaround 是连接 VPN 绕过 ISP 的过滤机制

这条经验说明冷基准(尤其是 Cold Resolution / Cold Installation)的方差来源常常不在被测工具本身,而在网络出口;复现时若发现某工具耗时方差异常,应先按此思路排查,再下工具快慢的结论。

七、小结与延伸阅读

  • 官方基准的四大场景定义、平台前提与限制,全部以 BENCHMARKS.md 为准:macOS + Python 3.12.4 环境、文件系统相关的安装策略差异、依赖集相关的瓶颈掩盖效应,是解读任何一张对比图之前必须内化的三条前提。
  • 复现入口是 scripts/benchmark 包(README 给出了最小示例),核心实现在 resolver.py,hyperfine 拼装在 scripts/benchmark/src/benchmark/init.py,图表渲染在 crates/uv-dev/src/render_benchmarks.rs
  • 文档本身也说明了其形式来源:BENCHMARKS.md 的设立受 Orogene 项目基准测试文档的启发(见 BENCHMARKS.md 的 Acknowledgements 一节)。
  • 若要对比 uv 不同构建版本的回归差异,可参考 scripts/benchmark/README.md 中“从多个分支构建二进制、再用多个 --uv-*-path 传入同一基准”的工作流。
登录后查看全文
热门项目推荐
相关项目推荐