PyPy:以最小的努力获得更快的Python

2020-11-11 22:42:39

Python是最受开发人员欢迎的编程语言之一,但它有一定的局限性。例如,根据应用程序的不同,它的速度可能是某些低级语言的100倍。这就是为什么一旦Python的速度成为用户的瓶颈,许多公司就会用另一种语言重写他们的应用程序。但是,如果有一种方法可以保持Python令人敬畏的特性并提高它的速度,那会怎样呢?输入PyPy。

PyPy是一个非常兼容的Python解释器,是CPython2.7、3.6以及很快的3.7的一个有价值的替代品。通过使用它安装和运行您的应用程序,您可以获得显著的速度提升。您将看到多大程度的改进取决于您正在运行的应用程序。

PyPy的功能是什么,以及它们如何让您的Python代码运行得更快。

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

许多实现都使用了Python语言规范,例如CPython(用C编写)、Jython(用Java编写)、IronPython(用.NET编写)和PyPy(用Python编写)。

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

然而,因为CPython是一种高级解释型语言,所以它有一定的局限性,不会在速度方面赢得任何奖牌。这就是PyPy可以派上用场的地方。因为它遵循Python语言规范,所以PyPy不需要更改代码库,并且可以显著提高速度,这要归功于您将在下面看到的特性。

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

您的操作系统可能已经提供了一个PyPy包。例如,在MacOS上,您可以在Homebrew的帮助下安装它:

如果没有,您可以下载适用于您的操作系统和体系结构的预构建二进制文件。下载完成后,只需解压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)[带有GCC 4.2.1的PyPy 7.3.1]键入";帮助";,";版权";,";信用";或";许可证";获取更多信息。

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

现在,您已经安装了PyPy,并且已经准备好看到它的实际应用了!为此,请创建一个名为script.py的Python文件,并在其中放入以下代码:

范围(1,10000)内的i合计=0 2:范围(1,10000)内的j的合计为3:4合计+=i+j 5 6打印(f";结果为{合计}";)。

这是一个脚本,在两个嵌套的for循环中,将1到9999之间的数字相加,然后打印结果。

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

1导入时间2 3开始时间=时间。Time()4 5 Total=06 for i in Range(1,10000):7 for j in Range(1,10000):8 Total+=i+j 9 10 print(f";结果为{Total}";)11 12 end_time=time。Time()13打印(f";计算花费了{end_time-start_time:.2f}秒)。

第13行打印start_time和end_time之间的差值,以显示运行脚本需要多长时间。

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

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

请记住,PyPy如何影响代码的性能取决于您的代码正在做什么。在某些情况下,PyPy实际上速度较慢,稍后您将看到这一点。然而,按几何平均计算,它的速度是Python的4.3倍。

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

但您不是早些时候被告知,PyPy是用Python编写的吗?嗯,这有点简单化了。PyPy之所以以用Python(而不是RPython)编写的Python解释器而闻名,是因为RPython使用与Python相同的语法。

代码中应用了RPython翻译工具链,这从根本上提高了代码的效率。它还将代码编译成机器码,这就是Mac、Windows和Linux用户必须下载不同版本的原因。

产生一个二进制可执行文件。这是您用来运行小脚本的Python解释器。

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

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

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

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

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

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

然而,性能仍然远远不及编译后的版本。原因是编译后的代码可以进行许多字节码无法实现的优化。

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

确定代码中最常用的组件,例如循环中的函数。

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

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

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

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

当大型对象树的引用计数变为零时,所有相关对象都会被释放。因此,您可能会有一个很长的暂停,在此期间您的程序根本没有进展。

此外,还有一个引用计数根本不起作用的用例。请考虑以下代码:

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

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

这就是CPython使用另一个称为循环垃圾收集器的工具的地方。它遍历内存中的所有对象,从已知根(如类型对象)开始。然后,它标识所有可到达的对象,并释放无法到达的对象,因为它们不再处于活动状态。这解决了基准周期问题。但是,当内存中有大量对象时,它可能会产生更明显的暂停。

另一方面,PyPy不使用引用计数。取而代之的是,它只使用了第二种技术,即周期搜索器。也就是说,它会定期从根开始遍历活动对象。这给了PyPy相对于CPython的一些优势,因为它不会费心进行引用计数,从而使内存管理所花费的总时间比CPython少。

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

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

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

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

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

话虽如此,核心团队正致力于C扩展。有些包已经移植到了PyPy上,运行速度也一样快。

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

你的车显然比你的脚快得多。但是,想一想这需要您做些什么:

开车要花很多开销,如果你想去的地方就在附近,这并不总是值得的!

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

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

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

1导入时间2 3开始时间=时间。在(100)范围内i的time()4 5:6 print(I)7 8 end_time=time。Time()9打印(f";计算";花费了{end_time-start_time:.10f}秒)。

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

PyPy是CPython的快速而强大的替代品。通过使用它运行脚本,您可以在不对代码进行任何更改的情况下获得显著的速度提升。但这不是灵丹妙药。它有一些限制,您需要测试您的程序,看看PyPy是否能提供帮助。

如果您的Python脚本需要稍微提高速度,那么可以尝试一下PyPy。根据您的程序不同,您可能会获得一些显著的速度提升!

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

每隔几天,你就会收到一份简短的、甜蜜的巨蟒小把戏,寄到你的收件箱里。从来没有过垃圾邮件。随时取消订阅。由Real Python团队策划。