❝原文:https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html
作者:Andrew Nesbitt
译者:Kurt Pan
uv 安装软件包的速度比 pip 快一个数量级。通常的解释是"它是用 Rust 写的"。这确实没错,但这并不能说明什么。很多工具都是用 Rust 写的,却并不特别快。真正有意思的问题是:究竟是哪些设计决策造成了这种差异。
Charlie Marsh 在 Jane Street 的演讲以及 Xebia 的工程深度剖析对技术细节做了很好的阐述。让我们来深入探讨那些促成这一切的设计决策:那些使快速路径成为可能的标准、uv 舍弃而 pip 支持的那些东西,以及那些根本不需要 Rust 的优化。
pip 的缓慢并非实现上的失败。多年来,Python 打包系统需要执行代码才能知道一个包需要什么依赖。
问题出在 setup.py。你不运行一个包的 setup 脚本就无法知道它的依赖。但你不安装它的构建依赖就无法运行它的 setup 脚本。PEP 518 在2016年明确指出了这一点:"你无法在不知道依赖的情况下执行 setup.py 文件,但目前没有标准方法能在不执行 setup.py 文件的情况下以自动化方式知道这些依赖是什么。"
这个先有鸡还是先有蛋的问题迫使 pip 不得不下载包、执行不可信代码、失败、安装缺失的构建工具,然后再次尝试。每次安装都可能是一连串的子进程派生和任意代码执行。安装一个源代码发行版本质上就是多了几个步骤的 curl | bash。
修复分阶段来到:
[project]表,使得依赖可以通过解析 TOML 而非运行 Python 来读取。 https://peps.python.org/pep-0621/PEP 658 于2023年5月在 PyPI 上线。uv 于2024年2月发布。这个时间点并非巧合。uv 之所以能够快,是因为生态系统终于有了支持它的基础设施。像 uv 这样的工具在2020年是不可能发布的。那时标准还不具备。
其他生态更早就想明白了这一点。Cargo 从一开始就有静态元数据。npm 的 package.json 是声明式的。Python 的打包标准终于让它达到了同等水平。
速度来自于消除。你没有的每一条代码路径都是你不需要等待的代码路径。
uv 的兼容性文档就是一份它不做的事情的清单:
不支持 .egg。Egg 是 wheel 之前的二进制格式。pip 仍然处理它们;uv 根本不尝试。这种格式已经过时十多年了。
不支持 pip.conf。uv 完全忽略 pip 的配置文件。不解析、不查找环境变量、不从系统级和用户级位置继承配置。
默认不编译字节码。pip 在安装期间将 .py 文件编译为 .pyc。uv 跳过这一步,为每次安装节省时间。如果你需要可以选择启用。
必须使用虚拟环境。pip 默认允许你安装到系统 Python。uv 反其道而行之,没有显式标志就拒绝触碰系统 Python。这消除了一整类权限检查和安全代码。
更严格的规范执行。pip 接受技术上违反打包规范的格式错误的包。uv 拒绝它们。更少的容忍意味着更少的回退逻辑。
忽略 requires-python 的上界。当一个包声明它需要 python<4.0时,uv 忽略上界,只检查下界。这大大减少了解析器的回溯,因为上界几乎总是错误的。包声明 python<4.0是因为它们还没有在 Python 4 上测试过,而不是因为它们真的会出问题。这种约束是防御性的,而非预测性的。
默认采用首个索引优先。当配置了多个包索引时,pip 会检查所有索引。uv 从第一个有该包的索引中选取,到此为止。这防止了依赖混淆攻击,也避免了额外的网络请求。
这些都是 pip 必须执行而 uv 不需要的代码路径。
uv 的一些速度确实来自 Rust。但没有你想的那么多。有几个关键优化今天就可以在 pip 中实现:
用于获取元数据的 HTTP range 请求。Wheel 文件是 zip 压缩包,而 zip 压缩包把文件列表放在末尾。uv 首先尝试 PEP 658 元数据,回退到对 zip 中央目录的 HTTP range 请求,然后是完整 wheel 下载,最后是从源码构建。每一步都更慢、风险更高。这种设计使快速路径覆盖了99%的情况。这是 HTTP 协议层面的工作,不是 Rust。
并行下载。pip 一次下载一个包。uv 同时下载多个。这是并发,不是语言魔法。
使用 hardlink 的全局缓存。pip 将包复制到每个虚拟环境中。uv 在全局保留一份副本并使用 hardlink(或在支持的文件系统上使用写时复制)。将同一个包安装到十个 venv 占用的磁盘空间与一个相同。这是文件系统操作,与语言无关。
无 Python 参与的解析。pip 做任何事都需要 Python 运行,并且会作为子进程调用构建后端来从遗留包获取元数据。uv 原生解析 TOML 和 wheel 元数据,只有当遇到只有 setup.py 别无选择的包时才派生 Python。
PubGrub 解析器。uv 使用 PubGrub 算法,该算法最初来自 Dart 的 pub 包管理器。pip 使用回溯解析器。PubGrub 在找到解决方案方面更快,在解释失败方面也更好。这是算法选择,不是语言选择。
有些优化确实需要 Rust:
零拷贝反序列化。uv 使用 rkyv 来反序列化缓存数据而无需复制。数据格式就是内存格式。这是 Rust 特有的技术。
无锁并发数据结构。Rust 的所有权模型使并发访问无需锁即可安全进行。Python 的 GIL 使这变得困难。
无解释器启动开销。pip 每次派生子进程都要付出 Python 的启动成本。uv 是一个单一的静态二进制文件,没有需要初始化的运行时。
紧凑的版本表示。uv 在可能的情况下将版本打包成 u64 整数,使比较和哈希变得快速。超过90%的版本可以放入一个 u64。这是微观优化,但在数百万次比较中会累积起来。
这些确实是真正的优势。但它们比放弃遗留支持和利用现代标准所带来的架构层面的收益要小。
uv 之所以快,是因为它不做的事情,而不是因为它用什么语言编写。PEP 518、517、621 和 658 的标准化工作使快速包管理成为可能。舍弃 egg、pip.conf 和宽松解析使其变得可实现。Rust 使它又快了一点。
pip 明天就可以实现并行下载、全局缓存和仅元数据解析。它之所以没有,很大程度上是因为与十五年来的边缘情况保持向后兼容优先级更高。但这意味着 pip 将永远比一个以现代假设从头开始的工具更慢。
对其他包管理器的启示是:使 uv 快速的那些东西是静态元数据、无需执行代码来发现依赖、以及能够在下载之前预先解析所有内容。Cargo 和 npm 多年来一直以这种方式运作。如果你的生态需要运行任意代码才能知道一个包需要什么,你就已经输了。