How uv got so fast
UV:Python 包管理器速度提升的秘诀
本文探讨了为什么 Python 包管理器 uv 比 pip 快一个数量级,并分析了其背后的设计决策,而非仅仅归功于 Rust 语言。
1. 标准化的基石
pip 速度慢的根源在于长期以来 Python 打包过程中执行代码以确定依赖项的需求。setup.py 脚本需要运行才能了解包的依赖关系,而运行它又需要安装构建依赖项,形成一个“鸡和蛋”问题。
为了解决这个问题,一系列 PEP 提案逐步完善了 Python 打包的标准:
- PEP 518 (2016): 引入
pyproject.toml文件,允许包声明构建依赖项,无需执行代码。 - PEP 517 (2017): 将构建前端与后端分离,避免 pip 需要理解 setuptools 的内部机制。
- PEP 621 (2020): 标准化
[project]表,允许通过解析 TOML 文件读取依赖项,而非运行 Python 代码。 - PEP 658 (2022): 将包元数据直接放入 Simple Repository API,允许解析器在下载 wheel 文件之前获取依赖信息。
这些标准的完善为 uv 的快速运行奠定了基础。其他生态系统(如 Cargo 和 npm)早已采用静态元数据,Python 也在逐渐赶上。
2. uv 舍弃的功能
uv 追求速度,关键在于减少不必要的代码路径:
- 放弃 .egg 支持: .egg 格式已过时。
- 忽略 pip.conf: 不解析 pip 的配置文件。
- 默认不进行字节码编译: 跳过编译 .py 文件到 .pyc 文件的步骤。
- 强制使用虚拟环境: 拒绝直接安装到系统 Python。
- 严格规范遵循: 拒绝不符合规范的包。
- 只检查版本下限: 忽略
requires-python中的版本上限。 - 优先使用第一个索引: 避免检查多个包索引。
3. 不依赖 Rust 的优化
一些关键的优化即使在 pip 中也能实现:
- HTTP 范围请求: 使用 HTTP 范围请求获取 wheel 文件的元数据,避免下载整个文件。
- 并行下载: 同时下载多个包。
- 全局缓存与硬链接: 使用硬链接或 copy-on-write 技术共享虚拟环境中的包,节省磁盘空间。
- Python-free 解析: 原生解析 TOML 和 wheel 元数据,减少 Python 进程的创建。
- PubGrub 解析器: 采用 Dart 包管理器的 PubGrub 算法,提高复杂依赖关系解析的速度。
4. Rust 的优势
Rust 确实带来了优势,但并非全部:
- 零拷贝反序列化: 使用 rkyv 库进行零拷贝反序列化。
- 线程级并行: Rust 可以原生实现线程级并行,避免 Python GIL 的限制。
- 无需解释器启动: uv 是一个单体静态二进制文件。
- 紧凑的版本表示: 将版本表示为 u64 整数,加速版本比较。
5. 设计的重要性
uv 的速度主要得益于其设计理念:舍弃旧的支持,利用现代标准。即使 pip 实现了并行下载、全局缓存和元数据解析,它也难以超越 uv,因为 pip 需要维护与 15 年历史遗留问题的兼容性。
文章强调,一个生态系统如果需要运行任意代码才能确定包的依赖关系,那么它就已落后。 像 Cargo 和 npm 那样的静态元数据、无需执行代码就能确定依赖关系的模式才是未来的趋势。