geekdoc-python-zh/docs/realpython/pypy-faster-python.md

15 KiB
Raw Permalink Blame History

PyPy:用最少的努力更快的 Python

原文:https://realpython.com/pypy-faster-python/

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 的最新版本。

Python 和 PyPy

Python 语言规范在许多实现中使用,如 CPython (用 C 编写)、Jython(用 Java 编写)、IronPython(用。NET),还有 PyPy(用 Python 写的)。

CPython 是 Python 的原始实现,是迄今为止最受欢迎和维护最多的。当人们提到 Python 时,他们通常指的是 CPython。你可能正在使用 CPython

但是因为是高级解释语言CPython 有一定的局限性,不会因为速度而得什么奖牌。这就是 PyPy 可以派上用场的地方。由于它符合 Python 语言规范PyPy 不需要对您的代码库进行任何更改,并且由于您将在下面看到的特性,它可以提供显著的速度改进。

现在,您可能想知道为什么 CPython 不实现 PyPy 的出色功能,如果它们使用相同的语法。原因是实现这些特性需要对源代码进行巨大的修改,这将是一项艰巨的任务。

不要深入理论,让我们看看 PyPy 的行动。

Remove ads

安装

您的操作系统可能已经提供了 PyPy 包。比如在 macOS 上,你可以借助家酿来安装:

$ brew install pypy3

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

$ 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 文件,并将以下代码放入其中:

 1total = 0
 2for i in range(1, 10000):
 3    for j in range(1, 10000):
 4        total += i + j
 5
 6print(f"The result is {total}")

这是一个脚本,在两个嵌套的 for循环中,将从19,999的数字相加,打印结果

要查看运行该脚本需要多长时间,请编辑它以添加突出显示的行:

 1import time 2
 3start_time = time.time() 4
 5total = 0
 6for i in range(1, 10000):
 7    for j in range(1, 10000):
 8        total += i + j
 9
10print(f"The result is {total}") 11
12end_time = time.time() 13print(f"It took {end_time-start_time:.2f} seconds to compute")

代码现在执行以下操作:

  • 第 3 行保存当前时间到变量start_time
  • 线 5 到线 8 运行循环
  • 第 10 行打印结果。
  • 第 12 行保存当前时间到end_time
  • 第 13 行打印出start_timeend_time之间的差异,以显示运行脚本花了多长时间。

尝试用 Python 运行它。这是我在 2015 年 MacBook Pro 上得到的结果:

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

现在用 PyPy 运行它:

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

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

对于更严肃的基准测试,您可以看看 PyPy 速度中心,在那里开发人员使用不同的可执行文件运行夜间基准测试。

请记住PyPy 如何影响代码的性能取决于您的代码在做什么。在某些情况下PyPy 实际上要慢一些,稍后您会看到。但是,在几何平均上,它是 Python 的 4.3 倍。

Remove ads

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 更好更快的特性。

实时(JIT)编译器

在进入什么是 JIT 编译之前,让我们后退一步,回顾一下 C 等编译的语言和 JavaScript 等解释的语言的属性。

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

还有像 Python 这样的混合编译和解释的编程语言。具体来说Python 首先被编译成一个中间字节码,然后由 CPython 解释。这使得代码比用纯解释编程语言编写的代码性能更好,并且保持了可移植性的优势。

然而,其性能仍然与编译版本相差甚远。原因是编译后的代码可以做很多优化,而字节码是不可能做到的。

这就是实时(JIT)编译器的用武之地。它试图通过编译成机器码和一些解释来获得两个世界的更好的部分。简而言之,以下是 JIT 编译提供更快性能的步骤:

  1. 确定代码中最常用的部分,例如循环中的函数。
  2. 在运行时将这些部分转换成机器代码。
  3. 优化生成的机器码。
  4. 用优化的机器码版本替换以前的实现。

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

垃圾收集

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

在 C 和 C++等编程语言中通常要手动处理这个问题。Python 和 Java 等其他编程语言会自动为您完成这项工作。这被称为自动垃圾收集,有几种技术可以实现它。

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

当一个大对象树的引用计数变为零时,所有相关对象都被释放。结果,你有一个潜在的长暂停,在此期间你的程序根本没有进展。

此外,在一个用例中,引用计数根本不起作用。考虑以下代码:

 1class A(object):
 2    pass
 3
 4a = A()
 5a.some_property = a
 6del a

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

此时,该实例不再可访问。但是,引用计数不会从内存中删除实例,因为它有对自身的引用,所以引用计数不为零。这个问题叫做引用周期,用引用计数是解决不了的。

这就是 CPython 使用另一个叫做循环垃圾收集器的工具的地方。它从已知的根开始遍历内存中的所有对象,比如type对象。然后,它识别所有可到达的对象,并释放不可到达的对象,因为它们已经不存在了。这解决了参考循环问题。但是,当内存中有大量对象时,它会造成更明显的停顿。

另一方面PyPy 不使用引用计数。相反,它只使用第二种技术,即循环查找器。也就是说,它周期性地从根开始遍历活的对象。这使得 PyPy 比 CPython 有一些优势,因为它不需要进行引用计数,使得花在内存管理上的总时间比 CPython 少。

此外PyPy 不是像 CPython 那样在一个主要的项目中做所有的事情,而是将工作分成可变数量的部分,并运行每个部分,直到一个也不剩。这种方法在每次小的收集后只增加几毫秒,而不是像 CPython 那样一次性增加数百毫秒。

垃圾收集很复杂,并且有更多的细节超出了本教程的范围。您可以在文档中找到关于 PyPy 垃圾收集的更多信息。

Remove ads

PyPy 的局限性

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

它不能很好地与 C 扩展一起工作

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

在这种情况下PyPy 团队建议去掉 CPython 扩展,用一个纯 Python 版本替换它,这样 JIT 就可以看到它并进行优化。如果这不是一个选项,那么您将不得不使用 CPython。

也就是说,核心团队正在开发 C 扩展。有些包已经移植到 PyPy 上,运行速度也一样快。

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

假设你想去一家离家很近的商店。你可以步行去,也可以开车去。

你的车显然比你的脚快得多。然而,想想它会要求你做什么:

  1. 去你的车库。
  2. 发动你的车。
  3. 把车预热一下。
  4. 开车去商店。
  5. 找个停车位。
  6. 回来的路上重复这个过程。

开车有很多开销,如果你想去的地方就在附近,那就不值得了!

现在想想如果你想去五十英里外的邻近城市会发生什么。开车去那里而不是步行肯定是值得的。

虽然速度上的差异不像上面的类比那样明显,但是 PyPy 和 CPython 也是如此。

当你用 PyPy 运行一个脚本时,它会做很多事情来使你的代码运行得更快。如果脚本太小,那么开销会导致脚本运行速度比在 CPython 中慢。另一方面,如果您有一个长时间运行的脚本,那么这种开销可以带来显著的性能收益。

要亲自查看,请在 CPython 和 PyPy 中运行以下小脚本:

 1import time
 2
 3start_time = time.time()
 4
 5for i in range(100):
 6    print(i)
 7
 8end_time = time.time()
 9print(f"It took {end_time-start_time:.10f} seconds to compute")

当你用 PyPy 运行它时,开始会有一点延迟,而 CPython 会立即运行它。确切地说,在 2015 款 MacBook Pro 上运行它需要0.0004873276秒,在 PyPy 上运行它需要0.0019447803秒。

它不做提前编译

正如您在本教程开始时看到的PyPy 不是一个完全编译的 Python 实现。它编译 Python 代码,但它不是 Python 代码的编译器。由于 Python 固有的动态性,不可能将 Python 编译成独立的二进制文件并重用它。

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

Remove ads

结论

PyPy 是 CPython 的快速而强大的替代品。通过用它来运行您的脚本,您可以在不对代码做任何改动的情况下获得很大的速度提升。但这不是银弹。它有一些限制,您需要测试您的程序,看看 PyPy 是否能有所帮助。

在本教程中,您学习了:

  • 什么是 PyPy
  • 如何安装 PyPy 和用它运行你的脚本
  • PyPy 在速度方面与 CPython 相比如何
  • PyPy 有什么特性以及它如何提高你程序的速度
  • PyPy 有哪些限制使它不适合某些情况

如果您的 Python 脚本需要一点速度提升,那么试试 PyPy 吧。根据你的程序,你可能会得到一些明显的速度提高!

如果你有任何问题,请在下面的评论区联系我们。****