PyPy加速Python:原理、性能对比与适用场景详解
本文深入解析PyPy如何通过JIT编译和优化的垃圾回收机制显著提升Python代码执行速度。通过对比CPython,展示在长时间运行脚本中的性能优势,并明确指出C扩展模块和短脚本场景下的局限性,帮助开发者合理选择解释器。
Python作为开发者广泛使用的编程语言,在某些场景下存在性能瓶颈,其运行速度可能比C/C++等语言低100倍。当速度成为应用瓶颈时,许多团队会选择用其他语言重写应用。然而,存在一种既能保留Python特性又能大幅提升执行效率的方案,即PyPy。
PyPy是一个高度兼容的Python解释器,可作为CPython 2.7、3.6以及即将发布的3.7版本的替代方案。通过替换解释器来运行现有应用程序,通常能带来显著的速度提升,具体加速幅度取决于应用程序的计算特征。
本教程将涵盖以下内容:
- 如何安装并运行PyPy环境
- PyPy与CPython在性能表现上的对比分析
- PyPy的核心功能及其加速Python代码的机制
- 本教程示例基于Python 3.6,这是PyPy目前兼容的最新Python版本。
PyPy简介
Python解释器有多种实现方式,包括用C编写的CPython、用Java编写的Jython、用.NET编写的IronPython,以及用Python编写的PyPy。
CPython是Python的最初实现,也是目前使用最广泛且维护最完善的版本。从Python官网下载安装Python 3.x后,默认获得的官方解释器即为CPython。因其由C语言开发而得名,在命令行输入python即启动该解释器。
由于CPython属于高级解释型语言,其在执行速度上存在固有局限,这正是PyPy发挥作用的地方。PyPy严格遵循Python语言规范,因此无需修改代码库即可直接运行,并通过特定机制显著提升执行效率。
既然CPython使用相同语法,为何未实现PyPy的高效功能?原因在于实现这些高级特性需要对源代码进行大规模重构,这是一项极其繁琐且复杂的工作。
接下来,我们将通过实际操作演示PyPy的使用方法。
安装
您的操作系统可能已预置PyPy软件包。例如,在Mac OS系统中,可通过Homebrew进行安装:
$ brew install pypy3
或者,您也可以下载与操作系统匹配的二进制文件。下载完成后,解压tarball或ZIP文件,并执行以下命令:
$ 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文件,并写入以下代码:
total = 0
for i in range(1, 10000):
for j in range(1, 10000):
total += i + j
print(f"The result is {total}")
该代码包含两个嵌套的for循环,用于计算1到9,999之间所有数字相加的总和,并打印最终结果。
接下来,查看运行该脚本所需的时间:
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")
该代码的具体执行逻辑如下:
- 第3行将当前时间戳保存至变量start_time。
- 第5至8行执行嵌套循环逻辑。
- 第10行打印计算结果。
- 第12行将结束时间戳保存为end_time。
- 第13行计算并打印start_time与end_time的差值,以此展示脚本运行耗时。
使用标准Python运行该脚本,以下是我在Mac Book 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 Speed Center查看更严谨的基准测试结果。
需注意,PyPy对代码性能的影响取决于具体的代码逻辑。在某些情况下,PyPy可能比标准Python更慢,这一点将在后文讨论。但从几何平均来看,其速度约为Python的4.3倍。
PyPy及其核心特性
PyPy有两个层面的定义:
1、一个用于生成动态语言解释器的动态语言框架;2、基于该框架实现的Python解释器。
您可能已经注意到第二点。您使用的Python解释器是基于名为RPython的动态语言框架编写的,这与CPython基于C语言、Jython基于Java语言编写类似。
前文曾提到PyPy是用Python编写的,这似乎存在矛盾。实际上,这种说法较为简化。PyPy之所以被称为“用Python编写的Python解释器”,是因为RPython采用了与Python相似的语法。
PyPy的生成过程涉及以下步骤:
1、其源代码由RPython编写。
2、RPython转换工具对代码进行处理,从根本上提升效率,并将其编译为机器代码。这就是Mac、Windows和Linux用户需下载不同版本二进制文件的原因。
3、通过上述方式生成的二进制可执行文件,即为用户运行的Python解释器。
普通用户无需执行上述所有步骤即可使用PyPy,因为官方已提供可直接安装和使用的可执行文件。
此外,由于框架和实现共用“PyPy”一词易造成混淆,PyPy团队决定弃用这种双重用法。现在,“PyPy”专指Python解释器,而框架被称为“RPython转换工具”。
接下来,我们将探讨在何种场景下使用PyPy比标准Python更优、更快。
Just-In-Time (JIT) 编译器
在理解JIT编译器之前,先回顾编译型语言(如C)与解释型语言(如JavaScript)的特性。
编译型语言在程序执行前需经过专门的编译过程,将源代码转换为机器语言文件(如exe)。运行时无需再次编译,直接执行结果即可,因此执行效率高。但这类语言通常与特定平台绑定,移植性较差。C、C++、Objective-C等均属于此类。
解释型语言无需预先编译,而是直接将源代码解释为机器码并立即执行。因此,只要平台提供相应解释器即可运行程序。由于每次运行都需解释源代码,效率较低;但因其具备平台无关性,便于程序移植。
Python属于混合模式,它首先将代码编译为字节码,再由CPython解释执行。这使得其性能优于纯解释型语言,同时保留了可移植性优势。
然而,其性能仍远低于编译型语言。原因在于,编译后的代码可执行多种优化,而字节码无法做到这一点。
JIT编译器应运而生,旨在通过部分编译和解释结合的方式,同时获取两种语言的优势。简而言之,JIT编译器通过以下步骤提升性能:
1、识别代码中最常用的组件,例如循环中的函数。
2、在运行时将这些组件转换为机器代码。
3、对生成的机器代码进行优化。
4、用优化后的机器代码版本替换原有实现。
还记得教程开头的两个嵌套循环吗?PyPy检测到重复执行相同操作时,会将其编译为机器代码,进行优化,然后替换原有实现。这也是您观察到显著加速结果的原因。
垃圾回收机制
每当创建变量、函数或对象时,计算机会为其分配内存。随着程序运行,部分对象将不再被需要。若不及时清理,可能导致内存耗尽并引发程序崩溃。
在C和C++等语言中,通常需手动管理内存。而Python和Java等语言则自动执行此操作,称为自动垃圾回收机制。
CPython采用引用计数技术。每当引用对象时,引用计数增加;取消引用时,计数递减。当计数为零时,CPython自动调用内存释放函数。这是一种简单有效的技术,但存在缺陷。
当大型对象树的引用计数变为零时,所有相关对象将被释放。这可能导致较长的暂停时间,期间程序无法执行任何操作。
此外,引用计数在某些情况下完全失效。示例如下:
class A(object): pass a = A() a.some_property = a del a
上述代码定义了新类,创建实例并将其指定为自身的属性,最后删除该实例。
此时,实例虽不可访问,但引用计数并未归零,因为它引用自身。此问题称为“引用循环”,无法通过引用计数解决。
为此,CPython引入了另一个工具:循环垃圾回收器。它从已知根(如类型对象)开始遍历内存中的所有对象,识别所有可访问对象,并释放不可访问的对象。这解决了引用循环问题,但当内存中存在大量对象时,可能导致更明显的暂停。
相比之下,PyPy不使用引用计数,而是仅使用第二种技术:循环查找器。它会定期从根开始遍历活动对象。这使得PyPy在内存管理方面比CPython更具优势,总耗时更少。
此外,PyPy将垃圾回收工作拆分为可变数量的部分,并逐一执行,直到无剩余部分。此方法仅在每次次要集合后增加几毫秒,而CPython可能一次性增加数百毫秒。
垃圾回收机制非常复杂,涉及诸多超出本教程范围的内容。有关PyPy垃圾回收机制的详细信息,请参阅官方文档。
PyPy的局限性
PyPy并非万能工具,不适合所有任务,甚至可能使应用程序执行速度比CPython慢得多。因此,必须注意以下局限性。
它不适用于C扩展
PyPy最适合纯Python应用程序。当使用C扩展模块时,其运行速度通常比CPython慢得多。原因是PyPy无法优化C扩展模块(因支持不完全),且必须模拟代码中的引用计数,导致性能下降。
在此情况下,PyPy团队建议移除CPython扩展并替换为纯Python版本。若无法实现,则必须继续使用CPython。
尽管如此,核心团队正在处理C扩展兼容性问题。部分软件包已移植至PyPy,且运行速度与CPython相当。
它仅适用于长时间运行的程序
想象一下,您想去附近商店。您可以选择步行或开车。
汽车速度明显快于步行,但需考虑以下步骤:
1.前往车库。
2、启动车辆。
3、预热车辆。
4、驾车前往商店。
5、寻找停车位。
6、返回途中重复上述过程。
开车涉及一系列繁琐步骤,若目的地近在咫尺,则未必值得。
现在考虑前往50公里外的邻近城市的情况。此时,开车显然比步行更值得。
虽然速度对比不如上述类比那样极端,但PyPy与CPython的关系与此道理相同。
使用PyPy运行脚本时,它会执行多项操作以加速代码。若脚本本身简单,实际运行速度可能低于CPython。反之,若脚本长时间运行,则可能带来显著性能提升。
若想亲身体验,请在CPython和PyPy中运行以下小脚本:
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会立即执行。在Mac Book Pro上测试,CPython耗时0.0004873276秒,PyPy耗时0.0019447803秒。
它不进行提前编译
正如教程开头所述,PyPy并非完全编译型的Python实现。它编译Python代码,但本身不是Python代码的编译器。由于Python固有特性,无法将Python编译为独立二进制文件并重复使用。
PyPy比完全解释型语言快,但比完全编译型语言(如C)慢。
总结
PyPy是CPython的一种快速且功能强大的替代方案。使用它运行脚本,可在不修改代码的情况下大幅提高速度。但它并非万能,存在特定局限性。
到此这篇关于如何用PyPy让你的Python代码运行得更快的文章就介绍到这了,更多相关Python PyPy 内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













