衔尾蛇?PyPy-更轻更快的Python(翻译)

本文最后更新于:2022年3月13日 下午

谈到 “Python” 时,我们一般指代的是使用 C 语言实现的 CPython,它有运行速度慢的不足。而提高 Python 代码运行速度的一个可能方案,就是改用 Python 实现的 Python —— PyPy。

PyPy 的 logo 是一条衔尾蛇,体现了“用Python写Python”

翻译信息

原文链接:PyPy: Faster Python With Minimal Effort - Real Python

作者:Jahongir Rahmonov

译者:muzing

翻译时间:2021.10

允许转载,转载必须保留全部翻译信息,且不能对正文有任何修改

Python 是开发人员中最常用的编程语言之一,但它确实有一定的局限性。例如,对于某些应用程序而言,它的运行速度可能比一些更底层的语言慢100倍。这就是为什么当 Python 的运行速度成为用户瓶颈后,许多公司会用另一种语言重写他们的应用程序。但是有没有一种方法既可以保持 Python 的特性又能提高其速度呢?它就是 PyPy

PyPy 是一种兼容性非常好的 Python 解释器,它是 CPython 2.7、3.6 和即将推出的 3.7 的一种值得选择的替代品。安装并用其运行您的应用程序,可以显著提高速度。速度能提高多少,取决于运行的应用程序。

在本教程中,您将学习:

  • 如何安装 PyPy 并用其运行您的代码
  • PyPy 与 CPython 在速度方面的比较
  • PyPy 的功能及其如何使Python代码更快地运行
  • PyPy 的局限性

本教程中的示例使用 Python 3.6,因为它是 PyPy 兼容的最新 Python 版本。

译者注:原文较旧了,2021年10月翻译时的最新 PyPy 版本为 3.8

Python 与 PyPy

Python 语言规范可以用多种语言来实现,例如 CPython(用 C 编写)、Jython(用Java 编写)、Iron Python(用 .NET 编写)和 PyPy(用 Python 编写)。

CPython 是 Python 解释器的最初实现,也是迄今为止使用最广泛、维护最完善的。当人们提到 Python 时,他们通常指的是 CPython。您可能正在使用 CPython。

但是,由于它是一种高级解释语言,因此 CPython 有一定的局限性,并且在速度方面没有任何优势。这就是 PyPy 可以派上用场的地方。由于它遵守 Python 语言规范,因此 PyPy 不需要对代码库进行任何更改,并且可以通过下面即将展示的特性显著提高运行速度。

现在,您可能想知道为什么 CPython 没有实现 PyPy 使用相同语法实现的出色功能。原因是实现这些功能需要对源代码进行大量的更改,这将是一项非常繁重的工作。

先不深入理论,让我们来看一下如何在实际操作中使用 PyPy。

安装

您的操作系统可能已提供 PyPy 软件包。例如,在 macOS上,您可以在 Homebrew 的帮助下安装它:

1
brew install pypy3

如果没有,您可以为您的操作系统和架构下载预构建的二进制文件。下载完成后,只需解压 tarball 或 ZIP 文件即可。然后你就可以执行 PyPy 而不需要在任何地方安装它:

1
2
3
4
5
$ tar xf pypy3.6-v7.3.1-osx64.tar.bz2
$ ./pypy3.6-v7.3.1-osx64/bin/pypy3
Python 3.6.9 (?, Jul 19 2020, 21:37:06)
[PyPy 7.3.1 with GCC 4.2.1]
Type "help", "copyright", "credits" or "license" for more information.

在执行上面的代码之前,您需要位于下载二进制文件的文件夹中。有关完整的说明,请参阅安装文档

运行 PyPy

您现在已经安装了 PyPy,并且即将运行它!为此,请创建一个名为 script.py 的 Python 文件,并将以下代码放入其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
import time

start_time = time.time()

total = 0
for i in range(1, 10000):
for j in range(1, 10000):
total += i + j

print(f"The result is {total}")

end_time = time.time()
print(f"It took {end_time-start_time:.2f} seconds to compute")

尝试用 Python 来运行它。下面是我在我的 2015 MacBook Pro 上获得的结果:

1
2
3
$ python3.6 script.py
The result is 999800010000
It took 20.66 seconds to compute

现在使用 PyPy 运行它:

1
2
3
$ pypy3 script.py
The result is 999800010000
It took 0.22 seconds to compute

在这个小型综合基准测试中,PyPy 的速度大约是 Python 的 94 倍!

对于更严格的基准测试,您可以查看 PyPy Speed Center,那里的开发人员使用不同的可执行文件运行基准测试。

请记住,PyPy 如何影响代码的性能取决于您用代码来做什么。在某些情况下,PyPy 实际上较慢,本文后面会提及。但是,就几何平均而言,它的速度是 Python 的4.3倍。

PyPy 及其特性

历史上,PyPy 指代两样东西:

  1. 用于生成动态语言解释器的动态语言框架
  2. 使用该框架的 Python 实现

通过安装 PyPy 并使用它运行一个小脚本,您已经看到了第二种含义。您使用的 Python 实现是使用称为 RPython 的动态语言框架编写的,就像 CPython 是用 C 编写的,而 Jython 是用 Java 编写的。

但之前文中不是提到 PyPy 是用 Python 编写的吗?嗯,这有点简化。PyPy 之所以被称为用 Python(而不是用 RPython)编写的 Python 解释器,是因为 RPython 使用与 Python 相同的语法。

为了澄清一切,以下是 PyPy 的生成方式:

  1. 源代码是用 RPython 编写的
  2. 代码中应用了RPython 翻译工具链,这基本上让代码效率更高。它还会将代码编译为机器码,这就是 Mac、Windows 和 Linux 用户必须下载不同版本的原因
  3. 生成二进制可执行文件。 这就是您用来运行小脚本的 Python 解释器

请记住,您无需执行所有这些步骤即可使用 PyPy。可执行文件已经可供您安装和使用。

此外,由于框架和实现使用同一个词非常令人困惑,PyPy 背后的团队决定摆脱这种双重用法。现在,PyPy 仅指 Python 实现。 该框架被称为 RPython 翻译工具链

接下来,您将了解在某些情况下使 PyPy 比 Python 更好更快的特性。

Just-In-Time (JIT) 编译器

在深入了解什么是 JIT 编译之前,让我们先后退一步,回顾一下以 C 为例的编译语言和以 JavaScript 为代表的解释型语言

编译型编程语言的性能更高,但更难移植到不同的 CPU 架构和操作系统。解释型编程语言具有更强的可移植性,但其性能比编译型语言差很多。 这是两个极端。

然后是诸如 Python 之类的编程语言,混合了编译和解释。具体来说,Python 先被编译成中间字节码(intermediate bytecode),然后由 CPython 解释。这使得代码比用纯解释性编程语言编写的代码性能更好,并且保持了可移植性上的优势。

但是它的性能仍然远远低于编译型语言。其原因是,编译后的代码可以执行许多优化,而字节码是无法做到的。

这就是 just-in-time (JIT) 编译器的用武之地。它尝试将一部分真正编译至机器码、一部分解释的方法,来结合两种优势。简而言之,以下是 JIT 编译为提供更快性能所采取的步骤:

  1. 识别代码中使用最多的组件,如循环中的函数
  2. 在运行时将这些部分转换为机器码
  3. 优化生成的机器码
  4. 用优化的机器码版本取代之前的实现

还记得教程开头的两个嵌套循环吗?PyPy 检测到重复执行相同操作时,将其编译为机器码,优化机器码,然后转换实现。这就是为什么速度有如此大的提升。

垃圾回收机制

每当创建变量、函数或任何其他对象时,计算机都会为它们分配内存。最终,其中一些对象将不再被需要。如果不及时清理它们,则计算机可能会耗尽内存并使程序崩溃。

在 C 和 C++ 等编程语言中,通常必须手动处理此问题。其他编程语言(如 Python 和 Java )则会自动执行此操作。这被称为自动垃圾回收机制,有多种技术可以实现。

CPython 使用一种称为引用计数的技术。本质上,Python 对象的引用计数在对象被引用时递增,在对象取消引用时递减。当引用计数为零时,CPython 会自动调用该对象的内存释放函数。这是一种简单而有效的技术,但存在一个问题。

当一个大型对象树的引用计数变为零时,所有相关对象都将被释放。结果可能会有很长的暂停时间,在此期间程序根本无法继续执行。

此外,还有一种情况,其中引用计数根本不起作用。考察下面的代码:

1
2
3
4
5
6
class A(object):
pass

a = A()
a.some_property = a
del a

在上面的代码中,定义了新的类。然后,创建一个该类的实例,并将其指定为其自身的属性。最后,删除实例。

此时,该实例将不再可访问。但是,引用计数不会从内存中删除实例,因为它具有对自身的引用,因此引用计数非零。此问题被称为引用循环,无法通过引用计数来解决。

这就是 CPython 使用的另一个工具——循环垃圾回收器之所在。它从已知的根(如类型对象)开始遍历内存中的所有对象。然后它标识所有可访问的对象,并释放不可访问的对象,因为它们不再存在。这样就解决了引用循环问题。但是,当内存中存在大量对象时,它会产生更明显的暂停。

另一方面,PyPy 不使用引用计数。相反,它只使用第二种技术,即循环查找器。也就是说,它会定期从根开始遍历活动对象。这使 PyPy 比 CPython 更具一些优势,因为它不需要考虑引用计数,从而使内存管理花费的总时间少于 CPython。

此外,PyPy 不是像 CPython 那样在一个主项中完成所有工作,而是将工作拆分为可变数量的多个部分,并运行每个部分,直到没有剩余为止。此方法只在每个次要集合之后只添加几毫秒,而不像 CPython 那样一次添加数百毫秒。

垃圾回收机制非常复杂,并且有许多超出本教程范围的细节。您可以在文档中找到有关 PyPy 垃圾回收机制的详细信息。

PyPy 的局限性

PyPy 并非灵丹妙药,可能并不总是最适合您任务的工具。它甚至可能使应用程序的执行速度比 CPython 慢得多。因此记住以下限制很重要。

不适用于 C 扩展

PyPy 最适合纯 Python 应用程序。无论何时使用 C 扩展模块,它的运行速度都要比在 CPython 中慢得多。原因是 PyPy 无法优化 C 扩展模块,因为它们不受完全支持。此外,PyPy 必须模拟代码中的引用计数,使其更慢。

在这种情况下,PyPy 团队建议去掉 CPython 扩展并将其替换为纯 Python 版本,以便 JIT 可以识别并优化它。如果不行的话,则必须使用 CPython。

尽管如此,核心团队正在处理 C 扩展。有些软件包已被移植到 PyPy,并且工作速度也同样快。

只适用于长时间运行的程序

想象一下您想去一家离你家很近的商店。您既可以直接步行,也可以驾车前往。

驾车明显比步行快得多。但是,请考虑需要您完成的步骤:

  1. 去车库
  2. 起动汽车
  3. 给车预热
  4. 开车去商店
  5. 寻找停车位
  6. 在返回途中重复此过程

开车需要一系列麻烦的步骤,如果想去的地方就在附近,那就不一定值得了。

现在想一下 50 公里外的邻近城市的情况。驾车去肯定是更合适的,而非步行。

虽然速度上的对比并不像上面的类比那样明显,但 PyPy 和 CPython 的情况与之类似。

当使用 PyPy 运行脚本时,它会执行许多操作以使代码运行得更快。如果脚本本身太小,则开销将导致实际脚本运行速度低于 CPython。另一方面,如果您有一个长时间运行的脚本,那么该开销会带来显著的性能提升。

想亲自感受一下的话,请在 CPython 和 PyPy 中运行以下小脚本:

1
2
3
4
5
6
7
8
9
import time

start_time = time.time()

for i in range(100):
print(i)

end_time = time.time()
print(f"It took {end_time-start_time:.10f} seconds to compute")

当您使用 PyPy 运行它时,开始时会有一个小延迟,而 CPython 会立即运行它。准确的数据是,在 2015 MacBook Pro 上运行它,用 CPython 需要 0.0004873276 秒,用 PyPy 则需要0.0019447803秒。

不做预先的编译

正如您在本教程开头所见,PyPy 不是一个完全编译型的 Python 实现。它编译 Python 代码,但它不是 Python 代码的编译器。由于 Python 固有的一些动态特性,导致将 Python 编译为独立的二进制文件并重用它是不可能的。

PyPy 是一个运行时解释器,比完全解释型的语言快,但比完全编译的语言(如 C)慢。

总结

PyPy 是 CPython 的一种快速且功能强大的替代方案。通过它运行脚本,您可以在不更改代码的情况下大大提高速度。但它也不是万能的。它有一些局限性,您需要测试您的项目以验证 PyPy 是否有帮助。在本教程中,您学习了:

  • PyPy 是什么
  • 如何安装 PyPy 并使用它运行脚本
  • PyPy 与 CPython 在速度方面的比较
  • PyPy 的特性及其如何提高程序速度
  • 在哪些情况下 PyPy 会有局限性

如果您的Python脚本需要稍微提高速度,欢迎尝试 PyPy。根据您的程序,您可能会得到一些显著的速度提升!


衔尾蛇?PyPy-更轻更快的Python(翻译)
https://muzing.top/posts/ceaa082/
作者
Muzing
发布于
2021年10月20日
更新于
2022年3月13日
许可协议