安装你的Python应用程序的依赖项可能会慢得令人吃惊。无论是在CI中运行测试、构建Docker镜像,还是安装一个应用程序,下载和安装依赖都会花费一些时间。
那么,你如何用pip 来加快安装速度呢?
在这篇文章中,我将介绍。
- 避免从源代码安装的缓慢路径。
pip下载速度,以及替代品。Pipenv和Poetry。- 一个有用的
pip选项,有时可以大大加快安装速度。
避免从源代码安装
当你安装一个 Python 包时,通常有两种安装方式。
- 打包后的源文件,通常是带有
setup.py的.tar.gz。在这种情况下,安装时往往需要运行 Python 代码 (有点慢),有时还需要编译大量的 C/C++/Rust 代码 (可能会非常慢)。 - 一个轮子 (
.whl文件),可以直接解压到文件系统上,不需要运行代码或编译本地扩展。
如果可能的话,你想安装轮子,因为从源代码安装会比较慢。如果你需要编译大量的C语言代码,从源码安装会更慢;你将需要自己编译,而不是依赖预编译的二进制文件。
为了确保你尽可能多地安装轮子。
- 在安装依赖项之前,确保你使用的是最新版本的
pip。二进制轮子有时需要比你当前 Python 默认打包的pip更新的版本。 - 不要使用 Alpine Linux;坚持使用使用
glibc的 Linux 发行版,例如 Debian/Ubuntu/RedHat/等等。标准的 Linux 轮子需要glibc,但 Alpine 使用muslC 库。像Alpine这样基于musl的发行版的轮子开始变得可用,但它们仍然不那么普遍。
比较pip 、Pipenv 和 Poetry 之间的安装速度
默认的 Python 软件包管理器是pip ,但你也可以使用Pipenv和Poetry,它们都增加了额外的功能,如 virtualenv 管理。我比较了这三者的速度。
方法论
安装Python软件包包括两个步骤。
- 下载软件包。
- 安装已经下载的包。
默认情况下,Python包管理器会将下载的包缓存在磁盘上,所以如果你在不同的virtualenv中第二次安装它们,包就不需要重新下载。因此,我测量了两种变体:一种是必须下载软件包的冷缓存,另一种是软件包在本地已经可用的暖缓存。
在所有情况下,我都确保提前创建了virtualenvs,对于pip ,我确保在requirements.txt ,以配合其他两个软件包管理器默认的哈希验证。
我在安装pandas 和matplotlib 时使用了横向依赖关系,结果是总共安装了12个不同的软件包。
结果
下面是每次安装所花的时间,同时测量挂钟和CPU时间。
| 工具 | 缓存 | 挂钟时间 | CPU时间 |
|---|---|---|---|
pip 22.1.1 | 寒冷 | 16.2s | 10.7s |
pip 22.1.1 | 暖气 | 10.5s | 9.4s |
| 管子2022.5.2 | 冷的 | 12.5s | 26.0s |
| 管道系统2022.5.2 | 暖气 | 9.7s | 25.2s |
| 诗歌 1.1.13 | 寒冷 | 12.1s | 18s |
| 诗歌1.1.13 | 温暖的 | 10.2s | 17.8s |
一些需要注意的事情。
pip当缓存是冷的时候,按壁钟时间计算是最慢的。- 当缓存是热的,即软件包已经被下载时,它们之间的壁挂时间其实并没有什么不同。
- Pipenv和Poetry都使用了并行性,我们可以从CPU时间高于壁时钟时间看出;
pip目前是单线程的。 - 与其他两个相比,Pipenv使用了相当多的CPU;Poetry要好一点,但仍然高于
pip。
这个例子是在安装了12个包的情况下运行的;如果有更多的依赖关系,Poetry的并行安装可能会有更大的影响。
保持缓存的温度
请注意,在所有情况下,你都会因为有一个温暖的缓存而得到加速,也就是说,重复使用已经下载的软件包。在你的本地机器上,这会自动发生。在大多数CI服务中,你的缓存一开始就是空的。
为了解决这个问题,大多数CI系统会有一些方法在运行结束后存储一个缓存目录,然后在下一次运行开始时加载。如果你使用的是GitHub动作,你可以使用用于设置Python的动作中的内置缓存支持。
但这仍然不如在专用机器上运行的速度快:存储和加载缓存也需要时间。
通过禁用版本检查来提高(非常小的)速度
在启动时,pip 可能会检查你是否在运行最新的版本,如果不是的话,会打印一个警告。你可以像这样禁用这个检查。
pip --disable-pip-version-check install ...
这为我节省了大约0.2-0.3秒,不是一个非常明显的改进;实际改进可能取决于你的网络速度和其他因素。
在禁用编译的情况下走得更快(有时)。
我们可以做得更好吗?在某些情况下,是的。
在软件包被下载(如果它们没有被缓存在本地)并安装到文件系统上之后,软件包管理器会做最后一步:它们将.py 源文件编译成.pyc 字节码文件,并将它们存储在__pycache__ 目录中。这与编译 C 语言扩展不一样,这只是一种优化,使 Python 代码的加载在启动时更快。不用在导入时编译.pyc ,而是.pyc 已经在那里了。
事实证明,字节码编译占用了pip install 的大量时间。但是你可以通过调用pip install --no-compile 来禁用这个步骤。
下面是有和没有.pyc 编译的软件包安装时间的比较,在这两种情况下,缓存是温暖的,所以不需要下载。
| 安装方法 | 缓存 | 挂钟时间 | CPU时间 |
|---|---|---|---|
pip install | 温热 | 10.5s | 9.4s |
pip install --no-compile | 暖机 | 4.8s | 4.0s |
那么,你应该总是使用这个选项吗?不一定。仅仅因为pip install 更快并不意味着你总体上节省了时间。
你导入的任何模块仍然需要被编译成.pyc ,只是这项工作将在 Python 运行时发生,而不是在软件包安装时。所以如果你导入了所有或大多数模块,总体上你可能根本就没有节省任何时间,你只是把工作转移到了不同的地方。
然而,在其他情况下,--no-compile 会为你节省时间。例如,在你的测试设置中,你可能为集成测试安装了许多第三方软件包,但只使用了这些库中的少量代码。因此,没有必要编译很多你不会使用的模块。
目前,Pipenv和Poetry似乎都不支持这个选项。
包的安装可以更快
考虑到有多少人使用Python,缓慢的软件包安装会增加。
很难估计世界上有多少pip install,但pip 本身在写这篇文章的前一个月被下载了一亿次,所以我们可以把它作为一个下限。如果你能从这1亿次安装中的每一次中减少1秒钟,那就意味着每个月可以节省3.17年的等待。
在Python世界中,软件包的安装显然有很大的改进空间。
- Poetry已经在某种程度上实现了并行化,但是考虑到比
pip更高的CPU使用率,它似乎并不像人们希望的那样高效。但是对于更多的依赖关系,它可能已经在壁挂式基础上更快了。 - Pipenv的CPU使用率甚至更糟。
至于pip 。
- 在一个默认使用多个CPU的世界里,单核速度的提高已经停滞不前,几乎所有基于CPU的任务
pip,都可以从并行化中受益。 - 并行下载和版本验证也会有帮助;对于小规模的软件包,网络延迟可能是瓶颈,而并行性可以帮助解决这个问题。
如果你有兴趣提供帮助,pip 仓库里有许多问题和正在进行的 PR,涵盖了各个方面。
最后,如果你维护开源的Python包:因为轮子的安装速度更快,确保为你的包提供轮子,即使它是纯Python。