uv 为何如此之快
Hacker News 摘要uv 的安装速度比 pip 快了一个数量级。虽然通常的解释是它使用了 Rust 编写,但这并不能解释全部。许多工具虽然也用 Rust 编写,但速度并不突出。uv 真正的优势在于其设计决策、对新标准的应用,以及对旧功能的舍弃。
使 uv 成为可能的标准
pip 的缓慢并非实现上的失败。多年来,Python 打包机制要求必须执行代码才能确定包的依赖项。核心问题在于 setup.py 文件:如果不运行安装脚本,就无法知道依赖;但不安装构建依赖,就无法运行脚本。这种死循环迫使 pip 必须下载包、执行不可信代码、失败后安装缺失工具再重试。每次安装都可能引发一系列子进程调用和任意代码执行。
解决这一问题的标准是分阶段完成的:
• PEP 518 (2016):引入了 pyproject.toml,允许包在不执行代码的情况下声明构建依赖。这种格式借用了 Rust 的 Cargo 模式。
• PEP 517 (2017):将构建前端与后端分离,使 pip 无需了解构建工具的内部细节。
• PEP 621 (2020):标准化了 [project] 表,使依赖项可以通过解析 TOML 直接读取,而无需运行 Python。
• PEP 658 (2022):允许直接在仓库 API 中提供元数据。解析器无需下载整个 Wheel 文件即可获取依赖信息。
PyPI 在 2023 年 5 月实现了 PEP 658,而 uv 在 2024 年 2 月发布。如果没有这些生态系统基础设施的完善,uv 这种工具在 2020 年是不可能实现的。
uv 舍弃的功能
速度往往源于简化。uv 通过消除不必要的路径来节省时间:
• 不支持 .egg 格式:这是一种过时的二进制格式,uv 完全不处理它。
• 忽略 pip.conf:uv 不读取 pip 的配置文件,减少了环境遍历和变量查找的开销。
• 默认不编译字节码:pip 在安装时会将 .py 编译为 .pyc,uv 默认跳过这一步。
• 强制使用虚拟环境:pip 默认允许安装到系统 Python 中,这涉及复杂的权限检查。uv 强制要求虚拟环境,移除了这些安全负担。
• 严格执行规范:pip 会容忍一些格式错误的包,而 uv 则直接拒绝。更少的容错逻辑意味着更快的执行路径。
• 忽略 Python 版本上限:当包声明需要 python<4.0 时,uv 通常会忽略上限。这减少了解析器的回溯次数,因为这种上限通常是出于防御性考虑而非真实限制。
• 首个索引优先:如果配置了多个包索引,pip 会检查所有索引。uv 默认在找到包的第一个索引处停止,避免了额外的网络请求。
与 Rust 无关的优化
uv 的一些核心性能提升实际上可以在 pip 中实现,而不需要依赖 Rust:
• 利用 HTTP 范围请求获取元数据:Wheel 文件是 Zip 存档,其文件列表位于末尾。uv 会尝试先获取 PEP 658 元数据,如果失败,则通过 HTTP 范围请求读取 Zip 的中央目录,最后才考虑下载整个文件。
• 并行下载:pip 逐个下载包,而 uv 并行下载。
• 带硬链接的全局缓存:pip 将包复制到每个虚拟环境中。uv 在全局保留一份副本,并使用硬链接(或写时复制)。在十个环境安装同一个包,只占用一份磁盘空间。
• 脱离 Python 的解析:pip 需要运行 Python 并启动子进程来获取元数据。uv 能够原生解析 TOML 和 Wheel 元数据,仅在遇到极其陈旧的 setup.py 包时才调用 Python。
• PubGrub 解析算法:uv 使用了源自 Dart 语言的 PubGrub 算法,它在寻找依赖方案和解释失败原因方面比 pip 的回溯解析器更高效。
Rust 真正发挥作用的地方
当然,有些优化确实受益于 Rust 的特性:
• 零拷贝反序列化:uv 使用 rkyv 库直接读取缓存数据,无需将其复制到内存。
• 无锁并发数据结构:Rust 的所有权模型保证了多线程访问的安全性,而无需像 Python 那样受制于全局解释器锁(GIL)。
• 无解释器启动开销:uv 是一个静态二进制文件,没有运行时初始化过程。而 pip 每次启动子进程都要支付 Python 解释器的启动成本。
• 紧凑的版本表示:uv 将包版本压缩为 64 位整数,使得数百万次的版本比较和哈希计算变得极快。
核心结论
uv 之所以快,是因为它站在了现代标准的肩膀上,并舍弃了对陈旧、复杂逻辑的兼容。pip 的缓慢在很大程度上是为了维持对过去十五年里各种边缘情况的后向兼容性。对于其他包管理器来说,真正的教训是:静态元数据、无需执行代码即可发现依赖以及预先解析的能力,才是性能的分水岭。如果一个生态系统必须运行代码才能知道一个包需要什么,那么它在速度竞争中就已经输了。