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

263 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PyPy:用最少的努力更快的 Python
> 原文:<https://realpython.com/pypy-faster-python/>
Python 是开发人员中最流行的编程语言之一,但它有一定的局限性。例如,根据应用的不同,它可以比一些低级语言慢 100 倍。这就是为什么一旦 Python 的速度成为用户的瓶颈,许多公司就会用另一种语言重写他们的应用程序。但是,如果有一种方法既能保留 Python 的出色特性,又能提高它的速度,那会怎么样呢?输入 [PyPy](https://doc.pypy.org/en/latest/introduction.html) 。
PyPy 是一个非常兼容的 Python 解释器,是 CPython 2.7、3.6 以及即将到来的 3.7 的有价值的替代品。通过使用它安装和运行您的应用程序,您可以获得显著的速度提升。您将看到多少改进取决于您运行的应用程序。
在本教程中,您将学习:
* 如何用 **PyPy** 安装并运行你的代码
* PyPy 在速度方面与 **CPython** 相比如何
* PyPy 的**特性**是什么,它们如何让你的 Python 代码运行**更快**
* PyPy 的**限制**是什么
本教程中的例子使用 Python 3.6,因为这是 PyPy 兼容的 Python 的最新版本。
## Python 和 PyPy
Python [语言规范](https://docs.python.org/3/reference/index.html)在许多实现中使用,如 [CPython](https://realpython.com/cpython-source-code-guide/) (用 [C](https://realpython.com/c-for-python-programmers/) 编写)、Jython(用 [Java](https://realpython.com/oop-in-python-vs-java/) 编写)、IronPython(用。NET),还有 PyPy(用 Python 写的)。
CPython 是 Python 的原始实现,是迄今为止最受欢迎和维护最多的。当人们提到 Python 时,他们通常指的是 CPython。你可能正在使用 CPython
但是因为是高级解释语言CPython 有一定的局限性,不会因为速度而得什么奖牌。这就是 PyPy 可以派上用场的地方。由于它符合 Python 语言规范PyPy 不需要对您的代码库进行任何更改,并且由于您将在下面看到的特性,它可以提供显著的速度改进。
现在,您可能想知道为什么 CPython 不实现 PyPy 的出色功能,如果它们使用相同的语法。原因是实现这些特性需要对源代码进行巨大的修改,这将是一项艰巨的任务。
不要深入理论,让我们看看 PyPy 的行动。
[*Remove ads*](/account/join/)
### 安装
您的操作系统可能已经提供了 PyPy 包。比如在 macOS 上,你可以借助[家酿](https://brew.sh/)来安装:
```py
$ brew install pypy3
```
如果没有,您可以[下载一个预构建的二进制文件用于您的操作系统和架构](https://www.pypy.org/download.html)。完成下载后,只需解压缩 tarball 或 ZIP 文件。然后,您可以执行 PyPy而不需要在任何地方安装它:
```py
$ 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.
```
在执行上面的代码之前,您需要进入下载二进制文件的文件夹。参考[安装文件](https://doc.pypy.org/en/latest/install.html)获取完整说明。
### PyPy 在行动
现在,您已经安装了 PyPy并准备好查看它的运行情况了为此创建一个名为`script.py`的 Python 文件,并将以下代码放入其中:
```py
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`循环](https://realpython.com/python-for-loop/)中,将从`1`到`9,999`的数字相加,[打印结果](https://realpython.com/python-print/)。
要查看运行该脚本需要多长时间,请编辑它以添加突出显示的行:
```py
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** 运行[循环](https://realpython.com/courses/how-to-write-pythonic-loops/)。
* **第 10 行**打印结果。
* **第 12 行**保存当前时间到`end_time`。
* **第 13 行**打印出`start_time`和`end_time`之间的差异,以显示运行脚本花了多长时间。
尝试用 Python 运行它。这是我在 2015 年 MacBook Pro 上得到的结果:
```py
$ python3.6 script.py
The result is 999800010000
It took 20.66 seconds to compute
```
现在用 PyPy 运行它:
```py
$ pypy3 script.py
The result is 999800010000
It took 0.22 seconds to compute
```
在这个小的合成基准测试中PyPy 的速度大约是 Python 的 94 倍!
对于更严肃的基准测试,您可以看看 PyPy [速度中心](https://speed.pypy.org/),在那里开发人员使用不同的可执行文件运行夜间基准测试。
请记住PyPy 如何影响代码的性能取决于您的代码在做什么。在某些情况下PyPy 实际上要慢一些,稍后您会看到。但是,在几何平均上,它是 Python 的 4.3 倍。
[*Remove ads*](/account/join/)
## PyPy 及其特性
历史上PyPy 提到了两件事:
1. 用于生成动态语言解释器的动态语言框架
2. 使用该框架的 Python 实现
通过安装 PyPy 并使用它运行一个小脚本,您已经看到了第二种含义。你用的 Python 实现是用一个叫做 [RPython](https://rpython.readthedocs.io/en/latest/index.html) 的动态语言框架写的,就像 CPython 是用 C 写的Jython 是用 Java 写的。
但是之前不是告诉你 PyPy 是用 Python 写的吗这有点简化了。PyPy 之所以被称为用 Python(而不是用 RPython)编写的 Python 解释器,是因为 RPython 使用与 Python 相同的语法。
为了澄清一切,下面是 PyPy 的生产过程:
1. 源代码是用 RPython 写的。
2. 将 [RPython 翻译工具链](https://rpython.readthedocs.io/en/latest/translation.html)应用于代码,这基本上使代码更加高效。它还将代码编译成机器码,这就是为什么 Mac、Windows 和 Linux 用户必须下载不同版本的代码。
3. 生成二进制可执行文件。这是您用来运行小脚本的 Python 解释器。
请记住,使用 PyPy 不需要经历所有这些步骤。该可执行文件已经可供您安装和使用。
此外由于在框架和实现中使用同一个词非常令人困惑PyPy 背后的团队决定不再使用这种双重用法。现在PyPy 仅指 Python 实现。该框架被称为 **RPython 翻译工具链**
接下来,您将了解在某些情况下使 PyPy 比 Python 更好更快的特性。
### 实时(JIT)编译器
在进入什么是 JIT 编译之前,让我们后退一步,回顾一下 C 等[编译的](https://en.wikipedia.org/wiki/Compiled_language)语言和 JavaScript 等[解释的](https://en.wikipedia.org/wiki/Interpreted_language)语言的属性。
编译过的编程语言性能更好,但是更难移植到不同的 CPU 架构和操作系统。**解释型**编程语言的可移植性更强,但性能却比编译型语言差很多。这是光谱的两个极端。
还有像 Python 这样的混合编译和解释的编程语言。具体来说Python 首先被编译成一个**中间字节码**,然后由 CPython 解释。这使得代码比用纯解释编程语言编写的代码性能更好,并且保持了可移植性的优势。
然而,其性能仍然与编译版本相差甚远。原因是编译后的代码可以做很多优化,而字节码是不可能做到的。
这就是**实时(JIT)编译器**的用武之地。它试图通过编译成机器码和一些解释来获得两个世界的更好的部分。简而言之,以下是 JIT 编译提供更快性能的步骤:
1. 确定代码中最常用的部分,例如循环中的函数。
2. 在运行时将这些部分转换成机器代码。
3. 优化生成的机器码。
4. 用优化的机器码版本替换以前的实现。
还记得教程开头的两个嵌套循环吗PyPy 检测到相同的操作被反复执行,将其编译成机器码,优化机器码,然后交换实现。这就是为什么你会看到速度有如此大的提高。
### 垃圾收集
每当你创建变量、[函数](https://realpython.com/defining-your-own-python-function/)或任何其他对象时,你的计算机都会给它们分配内存。最终,这些对象中的一些将不再需要。如果你不清理它们,那么你的计算机可能会耗尽内存,使你的程序崩溃。
在 C 和 C++等编程语言中通常要手动处理这个问题。Python 和 Java 等其他编程语言会自动为您完成这项工作。这被称为**自动垃圾收集**,有几种技术可以实现它。
CPython 使用一种叫做[引用计数](https://realpython.com/cpython-source-code-guide/#reference-counting)的技术。本质上Python 对象的引用计数在对象被引用时递增在对象被取消引用时递减。当引用计数为零时CPython 会自动调用该对象的内存释放函数。这是一种简单有效的技术,但是有一个问题。
当一个大对象树的引用计数变为零时,*所有相关对象都被释放。结果,你有一个潜在的长暂停,在此期间你的程序根本没有进展。*
此外,在一个用例中,引用计数根本不起作用。考虑以下代码:
```py
1class A(object):
2 pass
3
4a = A()
5a.some_property = a
6del a
```
在上面的代码中,您定义了新的类。然后,创建该类的一个实例,并将其分配为自身的一个属性。最后,删除实例。
此时,该实例不再可访问。但是,引用计数不会从内存中删除实例,因为它有对自身的引用,所以引用计数不为零。这个问题叫做**引用周期**,用引用计数是解决不了的。
这就是 CPython 使用另一个叫做[循环垃圾收集器](https://docs.python.org/3/c-api/gcsupport.html)的工具的地方。它从已知的根开始遍历内存中的所有对象,比如`type`对象。然后,它识别所有可到达的对象,并释放不可到达的对象,因为它们已经不存在了。这解决了参考循环问题。但是,当内存中有大量对象时,它会造成更明显的停顿。
另一方面PyPy 不使用引用计数。相反,它只使用第二种技术,即循环查找器。也就是说,它周期性地从根开始遍历活的对象。这使得 PyPy 比 CPython 有一些优势,因为它不需要进行引用计数,使得花在内存管理上的总时间比 CPython 少。
此外PyPy 不是像 CPython 那样在一个主要的项目中做所有的事情,而是将工作分成可变数量的部分,并运行每个部分,直到一个也不剩。这种方法在每次小的收集后只增加几毫秒,而不是像 CPython 那样一次性增加数百毫秒。
垃圾收集很复杂,并且有更多的细节超出了本教程的范围。您可以在[文档](https://doc.pypy.org/en/latest/gc_info.html)中找到关于 PyPy 垃圾收集的更多信息。
[*Remove ads*](/account/join/)
## PyPy 的局限性
PyPy 不是灵丹妙药,可能并不总是最适合您的任务的工具。它甚至可能使您的应用程序执行速度比 CPython 慢得多。这就是为什么记住以下限制很重要。
### 它不能很好地与 C 扩展一起工作
PyPy 最适合纯 Python 应用程序。无论何时你使用一个 [C 扩展模块](https://realpython.com/build-python-c-extension-module/),它的运行速度都比在 CPython 中慢得多。原因是 PyPy 不能优化 C 扩展模块因为它们不被完全支持。此外PyPy 必须模拟这部分代码的引用计数,这使得它更慢。
在这种情况下PyPy 团队建议去掉 CPython 扩展,用一个纯 Python 版本替换它,这样 JIT 就可以看到它并进行优化。如果这不是一个选项,那么您将不得不使用 CPython。
也就是说,核心团队正在开发 C 扩展。有些包已经移植到 PyPy 上,运行速度也一样快。
### 它只适用于长时间运行的程序
假设你想去一家离家很近的商店。你可以步行去,也可以开车去。
你的车显然比你的脚快得多。然而,想想它会要求你做什么:
1. 去你的车库。
2. 发动你的车。
3. 把车预热一下。
4. 开车去商店。
5. 找个停车位。
6. 回来的路上重复这个过程。
开车有很多开销,如果你想去的地方就在附近,那就不值得了!
现在想想如果你想去五十英里外的邻近城市会发生什么。开车去那里而不是步行肯定是值得的。
虽然速度上的差异不像上面的类比那样明显,但是 PyPy 和 CPython 也是如此。
当你用 PyPy 运行一个脚本时,它会做很多事情来使你的代码运行得更快。如果脚本太小,那么开销会导致脚本运行速度比在 CPython 中慢。另一方面,如果您有一个长时间运行的脚本,那么这种开销可以带来显著的性能收益。
要亲自查看,请在 CPython 和 PyPy 中运行以下小脚本:
```py
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*](/account/join/)
## 结论
PyPy 是 CPython 的快速而强大的替代品。通过用它来运行您的脚本,您可以在不对代码做任何改动的情况下获得很大的速度提升。但这不是银弹。它有一些限制,您需要测试您的程序,看看 PyPy 是否能有所帮助。
**在本教程中,您学习了:**
* 什么是 PyPy
* 如何**安装** PyPy 和**用它运行你的脚本**
* PyPy 在速度方面与 **CPython** 相比如何
* PyPy 有什么特性以及它如何提高你程序的速度
* PyPy 有哪些限制使它不适合某些情况
如果您的 Python 脚本需要一点速度提升,那么试试 PyPy 吧。根据你的程序,你可能会得到一些明显的速度提高!
如果你有任何问题,请在下面的评论区联系我们。****