用PyPy加速Python程序

简介: 我们可以用更好的Python运行环境或运行时优化来提升Python的速度,其中最成熟、使用最简单的当属PyPy。用PyPy,可以在不改变源代码的情况下,获得平均3-4倍的性能提升。本文将带大家学习如何用PyPy加速Python程序。

用PyPy加速Python程序

《Python性能优化指南--让你的Python代码快x3倍的秘诀》中有提到,我们可以用更好的Python运行环境或运行时优化来提升Python的速度,其中最成熟、使用最简单的当属PyPy。用PyPy,可以在不改变源代码的情况下,获得平均3-4倍的性能提升。本文将带大家学习如何用PyPy加速Python程序。

How-to-Go-Faster-With-PyPy_Watermarked.png

什么是PyPy

Python解释器有多种实现,比如

  • C语言实现的CPython;
  • Java实现的Jython ;
  • .NET实现的IronPython ;
  • Python实现的PyPy;

CPython是Python最原始的实现,也是迄今为止最流行和维护最多的实现。当我们说Python时,通常指的是CPython。你现在使用的Python大概率就是CPython。

然而,因为Python是高级解释型语言,CPython有其局限性,无法获得速度上的优势。而这恰恰是PyPy发挥优势的地方。由于PyPy遵循Python语言规范,不需要更改代码库,并且可以通过JIT显著提高速度。

这里您可能会问,既然CPython和PyPy实现的是相同的语法,为什么CPython不可以实现PyPy的强大功能?原因是,实现这些功能需要对源代码进行巨大的更改,这将是一项庞大到近乎不可能完成的任务。

作为Python的使用者(非维护者),这里我们不过多的深入到Python的底层和源代码,我们先看一下PyPy怎么用。

安装PyPy

有2种方法可以安装PyPy:

用包管理器安装

很多操作系统和Linux发行版都带有PyPy包。

如果是Ubuntu,可以用apt安装

$ sudo add-apt-repository ppa:pypy/ppa
$ sudo apt update
$ sudo apt install pypy3

如果是CentOS,可以用yum安装,以CentOS 7.4为例

$ rpm -ivh http://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/e/epel-release-7-11.noarch.rpm
$ yum -y install pypy-libs pypy pypy-devel

如果是MacOS,可以通过 Homebrew安装

$ brew install pypy3

用预编译好的安装包安装

预编译好的安装包可以通过这里下载,你可以根据你的环境下载对应的安装包。

pypy_01.png

如果是Windows系统,下载好安装包后解压安装就可以了。

如果是Linux,直接解压安装包,

$ tar xf pypy3.9-v7.3.9-linux64.tar.bz2
$ ./pypy3.9-v7.3.9-linux64/bin/pypy3
Python 3.9.10 (?, Mar 16 2022, 16:03:21)
[PyPy 7.3.9 with GCC 4.2.1]
Type "help", "copyright", "credits" or "license" for more information.

然后在/usr/local/bin中创建一个pypy的链接。

使用PyPy

PyPy安装好后,我们实际测试一下PyPy对Python程序运行速度的提升。我们的测试脚本test.py如下:

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

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

上面的代码用了双重循环,每重循环从[1, 9999],然后输出累加结果。我们用cProfile看一下执行效率:

用CPython执行

$ python -m cProfile test.py

pypy_03.png

用PyPy执行

$ pypy3 -m cProfile test.py

pypy_02.png

通过上面的简单测试我们能看到,CPython虽然运行时栈很简单,只有4个函数调用,但是总共用时15.509秒;而PyPy调用了303个函数,但总共用时5.618秒。PyPy比CPython快3倍!

更多性能对比可以参看PyPy的Speed Center,这里有大量性能对比数据。

这里需要注意的是,PyPy对代码速度的提升不是稳定一成不变的,而是跟代码的功能有密切关系。某些情况下PyPy的速度反而更慢。但是平均来讲PyPy比CPython快4.3倍

PyPy的构建

我们习惯说PyPy是用Python写的,于是很多人以为PyPy是Python语言自举的产物。这个观点是不对的。这里我专门为大家纠正一下这个错误观念。

PyPy在建立之初有两个目标:

  1. 创建一个能够为动态语言生成解析器的动态语言框架
  2. 其次才是用这个框架实现一个Python解析器。

第二点目标的产物就是PyPy,上一节我们已经体验了PyPy的性能威力。第一点目标中的动态语言框架叫RPython,PyPy就是用RPython写的。

开篇提到的PyPy是用Python写的其实不准确,这只是习惯性的简略说法。我们习惯说PyPy是用Python写的是因为RPython跟Python有着相同的语法,PyPy实际是用RPython写的。为了解除大家这里的迷惑,这里简单介绍一下PyPy是怎样开发出来的:

  1. 首先用RPython写PyPy的源代码;
  2. 然后用 RPython翻译工具翻译转换源代码,让代码更高效。这个过程会将源代码编译成机器码。这就是为什么Windows, Mac,Linux要分别下载对应操作系统的安装包;
  3. 生成二进制可执行文件,这就是pypy3。

为了避免混淆,这里梳理一下几个易混淆的名词:

  • PyPy指的Python解释执行器,是Python的一个具体实现。
  • RPython是一门语言,语法跟Python一样。
  • RPython翻译工具是生成动态语言解释执行器的框架。

PyPy是用RPython写的,用RPython翻译工具生成的一个Python实现。

PyPy性能优化关键技术

JIT(Just-In-Time)编译

你可能之前听说过JIT技术,他是一种介于编译型语言(比如C)和解释型语言(比如Python)之间的中间语言技术。编译型语言编译器会预先将源代码编译成二进制机器码,CPU可以直接执行,因此执行速度快,但缺点是可以执行差;而解释型语言可移植性好,但执行速度慢,需要解释器一行一行解释执行。

编译型语言和解释型语言是天平的两端,一个的优势就是另外一个的劣势。有没有办法能将兼得二者的优势呢?于是就出现了中间解决方案。Python就结合了编译型语言和解释型语言,先将源代码编译成中间字节码,然后再由CPython解释执行。这样即能兼顾可以执行,同时性能比纯解释型语言好很多。

然而,即便这样,中间字节码的执行效率依然无法与编译成机器码同日而语。其中一个原因是编译器可以对编译的机器码做很多优化,而自己码能做的就很有限。

此时JIT技术就应运而生,它试图整合机器码和解释器二者的优势。简而言之,JIT通过如下步骤提升程序性能:

  1. 识别代码中最常用的部分,例如循环中的函数;
  2. 在运行时将这些部分转换为机器码;
  3. 优化生成的机器码;
  4. 用优化过的机器码替换原来的实现。

下图是编译型语言、解释型语言和JIT编译的开发、执行流程对比图

pypy_06.png

我们前面给出的示例程序中使用了双重循环,PyPy检测到相同的操作正在反复执行,于是将其编译成机器代码并加以优化,然后用优化后的机器码替换原来的Python实现,从而后面的执行全部执行的是速度更快的机器码。这就是为什么用PyPy执行速度有如此大的提高。

垃圾回收

我们都知道,在程序中创建变量、方法以及一切对象都需要分配内存。C和C++需要我们手动管理内存的分配和释放,而Python提供了垃圾回收(GC)机制自动帮我们管理内存。

垃圾回收有多种实现方法和策略。

CPython用的技术叫“引用计数”。简单的说就是,当对象被引用时引用计数增加,对象被取消引用时引用计数减少。当引用计数为0时,CPython会自动调用该对象的内存释放函数释放内存。这是一种简单有效的技术,很多编程语言(如Objective-C,Swift, Rust中的Rc/Arc)都采用这种方法管理内存。但它存在一个缺点,那就是“全局暂停”

想象一下,当一个大型对象树的根节点引用计数变为0时,此时需要释放树上所有相关对象。此时程序需要大量执行内存释放函数,无法执行任何其他功能,从而导致程序像暂停了一样。这个现象就叫“全局暂停”。

除了大量释放内存会带来全局暂停外,“循环引用”会带来更严重的全局暂停。

循环引用顾名思义就是A引用了B,B又引用了A,他们构成了一个环。此时要释放A需要解除B对它的引用,而要释放B需要解除A对它的引用,这样就构成了无限循环。

企业微信截图_20230519190848.png

为了解决循环引用问题,CPython引入了循环垃圾回收器。循环垃圾回收器会从已知的根开始遍历内存中的所有对象,然后识别所有可到达的对象,并释放无法到达的对象,因为这些不可达的对象已经不在树上,说明他们不会再被用到。这样就解决了循环引用的问题。然而,当内存中存在大量对象时,循环垃圾回收会产生更明显的暂停。

而PyPy在内存管理上没有采用“引用计数”技术,而是采用了“循环查找器”技术。循环查找器会周期性地从根开始遍历活动对象,不依赖对象的引用计数,从而降低了内存管理上的花销。

此外,PyPy没有像CPython那样在一个主任务中完成所有工作,而是将工作拆分为数量可变的子任务,然后运行每个子任务,直到所有子任务都运行完成。在每个子任务完成后都会执行一次垃圾回收,这样做可以避免内存中出现大型对象树,从而减少垃圾回收带来得全局暂停。

PyPy的局限

PyPy性能这么好为什么不像CPython那么普及?这是因为PyPy存在一些局限。这些局限导致有些任务场景下PyPy并不适用。

C语言扩展支持不佳

PyPy非常适合运行纯Python程序,如果程序中用到了C语言扩展,PyPy的运行速度比CPython慢很多。这是因为PyPy对C语言扩展的支持不完整,也缺乏相应的优化。并且,对C语言扩展的支持需要PyPy执行引用计数,这进一步减缓了PyPy的速度。

对于这一点,PyPy团队的建议是移除C语言扩展,用纯Python代码来实现,这样就可以使用JIT优化。否则,就乖乖用CPython。

尽管如此,PyPy核心团队正在研究C语言扩展的移植。一些包已经移植到PyPy,工作速度一样很快。

不适合短时间运行场景

举个例子,假如你要去家旁边的超时购物,你会步行去还是开车去?开车速度肯定比不行速度快,但开车要去停车场、发动车子,到了目的地要找停车位停车;回程还要缴费、重新将车停好。这些开车带来的附加工作所消耗的时间可能比步行过去还要多。

PyPy也是如此。当我们用PyPy运行Python脚本时,PyPy会做大量工作让程序能跑得快一点。但如果程序运行时间很短,那么PyPy为速度优化而做出的开销反而会让它的总运行时间比CPython更长。我们可以用之前的例子,将10000改为100,在对比一下CPython和PyPy的运行时间:

用CPython执行

pypy_04.png

用PyPy执行

pypy_05.png

从上面的输出可以看出,CPython用时0.001秒,而PyPy用时0.003秒。这次变成CPython比PyPy快三倍!

但如果我们的程序需要长时间运行,那么PyPy为速度优化做出的开销就很值得,能换来更短的总运行时间。

无法实现提前编译

如前面所讲,PyPy不是一个完全编译的Python实现。它会在运行时编译Python代码,但不是Python代码的编译器。由于Python固有的动态性,不可能将Python编译成独立的二进制文件并重用它。

这就导致PyPy作为一种运行时解释器,他的性能比纯解释器(比如CPython)要快,但跟编译型语言(比如C语言)比还是慢很多。

总结

PyPy是CPython的一个快速而强大的替代品。用PyPy,你可以在不改动源代码的情况下获得约3倍左右的速度提升。然而,PyPy不是银弹,它有其局限性,实际项目中用PyPy是否能获得实质性的提升要看具体代码功能的情况,建议可以用PyPy做一下测试,可能会收获显著的性能提升。

目录
相关文章
|
3天前
|
机器学习/深度学习 数据挖掘 Python
Python编程入门——从零开始构建你的第一个程序
【10月更文挑战第39天】本文将带你走进Python的世界,通过简单易懂的语言和实际的代码示例,让你快速掌握Python的基础语法。无论你是编程新手还是想学习新语言的老手,这篇文章都能为你提供有价值的信息。我们将从变量、数据类型、控制结构等基本概念入手,逐步过渡到函数、模块等高级特性,最后通过一个综合示例来巩固所学知识。让我们一起开启Python编程之旅吧!
|
3天前
|
存储 Python
Python编程入门:打造你的第一个程序
【10月更文挑战第39天】在数字时代的浪潮中,掌握编程技能如同掌握了一门新时代的语言。本文将引导你步入Python编程的奇妙世界,从零基础出发,一步步构建你的第一个程序。我们将探索编程的基本概念,通过简单示例理解变量、数据类型和控制结构,最终实现一个简单的猜数字游戏。这不仅是一段代码的旅程,更是逻辑思维和问题解决能力的锻炼之旅。准备好了吗?让我们开始吧!
|
5天前
|
机器学习/深度学习 数据挖掘 开发者
Python编程入门:理解基础语法与编写第一个程序
【10月更文挑战第37天】本文旨在为初学者提供Python编程的初步了解,通过简明的语言和直观的例子,引导读者掌握Python的基础语法,并完成一个简单的程序。我们将从变量、数据类型到控制结构,逐步展开讲解,确保即使是编程新手也能轻松跟上。文章末尾附有完整代码示例,供读者参考和实践。
|
15天前
|
机器学习/深度学习 算法 编译器
Python程序到计算图一键转化,详解清华开源深度学习编译器MagPy
【10月更文挑战第26天】MagPy是一款由清华大学研发的开源深度学习编译器,可将Python程序一键转化为计算图,简化模型构建和优化过程。它支持多种深度学习框架,具备自动化、灵活性、优化性能好和易于扩展等特点,适用于模型构建、迁移、部署及教学研究。尽管MagPy具有诸多优势,但在算子支持、优化策略等方面仍面临挑战。
41 3
|
17天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
18 3
|
20天前
|
存储 人工智能 数据挖掘
Python编程入门:构建你的第一个程序
【10月更文挑战第22天】编程,这个听起来高深莫测的词汇,实际上就像搭积木一样简单有趣。本文将带你走进Python的世界,用最浅显的语言和实例,让你轻松掌握编写第一个Python程序的方法。无论你是编程新手还是希望了解Python的爱好者,这篇文章都将是你的理想起点。让我们一起开始这段奇妙的编程之旅吧!
20 3
|
10天前
|
存储 机器学习/深度学习 搜索推荐
Python编程入门:从零开始构建你的第一个程序
【10月更文挑战第32天】本文旨在通过浅显易懂的方式引导编程新手进入Python的世界。我们将一起探索Python的基础语法,并通过实例学习如何构建一个简单的程序。文章将不直接展示代码,而是鼓励读者在阅读过程中自行尝试编写,以加深理解和记忆。无论你是编程初学者还是希望巩固基础知识的开发者,这篇文章都将是你的良师益友。让我们开始吧!
|
1月前
|
IDE 开发工具 Python
Python 编程入门:打造你的第一个程序
【10月更文挑战第6天】编程,这个听起来高大上又充满神秘感的领域,其实就像学习骑自行车一样。一开始你可能会觉得难以掌握平衡,但一旦你学会了,就能自由地穿梭在广阔的道路上。本文将带你走进 Python 的世界,用最简单的方式让你体验编写代码的乐趣。不需要复杂的理论,我们将通过一个简单的例子——制作一个猜数字游戏,来实践学习。准备好了吗?让我们开始吧!
|
1月前
|
存储 JSON 安全
面向企业应用程序的 Python 配置管理
面向企业应用程序的 Python 配置管理
28 9
|
1月前
|
人工智能 数据挖掘 程序员
Python 编程入门:打造你的第一个程序
【10月更文挑战第3天】编程,这个看似高深莫测的技能,实际上就像学骑自行车一样,一旦掌握,便能开启全新的世界。本文将带领初学者步入Python编程的殿堂,从基础语法到编写实用程序,一步步解锁编程的乐趣。