Python性能优化-PyPy

  |  

摘要: 《Python性能分析与优化》笔记,JIT 层面怎样做 Python 性能优化,以 PyPy 为例。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


写在前面

这是 Python 性能分析与优化系列的第 9 篇文章,主要参考 Python性能分析与优化 这本书。

往期回顾

第一期: Python性能分析基础
第二期: Python性能分析器 — cProfile
第三期: 使用cProfile进行性能分析与优化实践
第四期: Python性能分析器 — line_profiler
第五期: line_profiler性能分析实践 — 优化倒排索引
第六期: Python性能分析-可视化
第七期: Python性能优化-优化每一个细节
第八期: Python性能优化-多进程与多线程

在前面若干期中,我们都是在优化 Python 众多实现中的一种(CPython)。Python 还有其他实现方式,在这一期我们将学习其中的两种。

  • PyPy: 它是Python解释器的另一个版本。
  • Cython: 它是一种优化过的静态编译器,可以让我们写静态代码,并轻松借助C/C++的力量。

本文主要围绕 PyPy 这种方法。


PyPy

PyPy 是 Python 的另一种实现,关于 PyPy 项目的历史,可以参考 项目主页

选择PyPy作为优化方法的若干理由:

  • 速度: PyPy的一个主要特性是对普通Python代码运行速度的优化。这是由于它使用JIT(Just-in-time)编译器。在静态编译代码时它提供了一种灵活性,可以在运行时根据运行环境(处理器、操作系统版本等)进行调整。另一方面,静态编译程序可能需要一个可执行或者不同条件的组合体。
  • 内存: PyPy执行脚本时消耗的内存要比普通Python小。
  • 沙盒(sandboxing): PyPy提供了沙盒环境,在调用C语言库的时候使用。这种机制会与一个处理实际情况的外部进程通信。虽然这种机制很好,但还只是一个原型,需要一些处理才能正常使用。

1. 安装

可以直接从网页下载可执行文件。

在 linux 中,也可以从安装包仓库中安装。

1
2
sudo apt install pypy3
sudo snap install pypy3

2. JIT 编译器

PyPy 主要特性之一。是 PyPy 速度远胜 CPython 的关键。

普通版Python的编译器在程序第一次运行之前,要把全部源代码都转换成机器码。

JIT编译是指源代码编译是在运行时同时进行的,而不像标准编译器那样在运行前进行。代码的处理方式分成两步

  1. 首先,源代码被翻译成一种中间语言代码,比如 Java 里的字节码就是中间代码。
  2. 有了字节码之后,我们开始把它编译并翻译成机器码,但是按需翻译。JIT编译器的特性之一就是,只编译需要运行的那部分代码,不是一次性全编译。

JIT编译器还会缓存已编译的代码,这样在下一次编译时可以避免多余的消耗。

注意:如果要利用JIT编译器,就必须运行几秒钟,以便指令缓存起作用。

使用JIT编译器的一个主要优势是,被执行的程序可以在具体的操作系统上优化机器码(包括CPU、操作系统等)。因此它实现了一种普通静态编译程序(甚至解释型程序)无法获得的灵活性。

3. 沙盒

沙盒可以理解成是一个安全运行环境,不安全的Python代码也可以在其中运行,不必担心其会损坏整个宿主系统。

PyPy的沙盒通过一个双进程模型实现。

(1) 一方面,我们有一个自定义的PyPy专门编译沙盒模型中的函数也就是说任何库或系统调用(例如I/O操作),都会在一个stdout里排列好等待响应

(2) 另一方面,我们有一个容器进程,可以用PyPy或CPython运行这个进程主要是响应PyPy内部进行的库和系统调用

一段Python代码在沙盒模式中运行,并执行一个外部库调用的过程

容器进程就是决定使用哪种虚拟化的进程。这个进程是沙盒进程与操作系统的中间层。

4. JIT优化

(1) 针对函数优化

JIT可以分析函数热度,即判断哪个函数比其他函数”更热”

例子:对比两种函数的使用方式, 内联和封装

CPython 运行结果

1
2
No function: 6.458110
Function: 7.329655

PyPy 运行结果(pypy3 file.py)

1
2
No function: 0.921062
Function: 0.896653
  • 用 PyPy,则封装的方式更快。用 CPython,内联的方式更快。
  • 同样的代码 PyPy 比 CPython 更快
  • JIT 会实时优化代码而 CPython 不会

例子:对比连接同样字符串的三种方式

在Python里字符串是不可变对象。因此,如果我们想把大量的字符串连接成一个对象,最好是换一种数据类型,而不是用原来的字符串类型。PyPy 里也一样。

因为一些CPython常用的C语言库同样支持PyPy,因此可以用 cStringIO (在 Python3 中是 io.StringIO)。

下面对比三种方法连接字符串的时间消耗

  1. 用字符串
  2. cStringIO
  3. 列表连接字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from io import StringIO
import time
import io

TIMES = int(2e5)

start = time.process_time()
value = ""
for i in range(TIMES):
value += str(i)
print("Concatenation: {:.6f}".format(time.process_time() - start))

start = time.process_time()
value = StringIO()
for i in range(TIMES):
value.write(str(i))
print("StringIO: {:.6f}".format(time.process_time() - start))

start = time.process_time()
value = []
for i in range(TIMES):
value.append(str(i))
finalValue = "".join(value)
print("List: {:.6f}".format(time.process_time() - start))

CPython 的运行结果

1
2
3
Concatenation: 0.084656
StringIO: 0.093116
List: 0.080741

PyPy 的运行结果

1
2
3
Concatenation: 32.514490
StringIO: 0.009878
List: 0.026609
  • PyPy 在使用字符串的方法上性能极差
  • PyPy 上 StringIO 的优化效果更好
  • CPython 上列表 join 的优化效果更好

5. 禁止 JIT

下面三种方法会通过sys模块禁止JIT效果

  • _getframe
  • exc_info
  • settrace

6. PyPy 例子: 反复调用同一个函数

在for循环中重复500万次。反复地调用同一个函数。此时 CPython 解释器并不合适。

反复调用函数的代码可以用 PyPy 的 JIT 优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import math
import time

def calcualte_acos(a, b ,theta):
return math.acos((math.cos(a)*math.cos(b)) +
(math.sin(a) * math.sin(b) * math.cos(theta)))

def great_circle(lon1, lat1, lon2, lat2):
radius = 3956 # miles
x = math.pi / 180.0

a = (90.0 - lat1) * (x)
b = (90.0 - lat2) * (x)
theta = (lon2 - lon1) * (x)
c = calcualte_acos(a, b, theta)
return radius * c

lon1, lat1, lon2, lat2 = -72.345, 34.323, -61.823, 54.826
num = int(5e6)

start = time.process_time()
for i in range(num):
great_circle(lon1, lat1, lon2, lat2)
print("time: {:.6f}".format(time.process_time() - start))

Cpython 运行 7s,PyPy 运行 0.18s。


Share