Python性能优化-Cython-1

  |  

摘要: 《Python性能分析与优化》笔记,Cython 层面如何做 Python 的性能优化。第一部分

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


写在前面

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

往期回顾

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

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

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

本文主要围绕 Cython 这种方法。Cython 的内容是很庞大的,详细内容可以参考 Cython User Guide。这里只关注一些皮毛,看看是怎么用的。


0. Cython

Cython 并没有使用另一种与 CPython 不同的解释器,但可以让我们直接将 Python 代码编译成 C 语言。(Cython 是一个转换器)

Cython 把 Python 的超集翻译成 C/C++ 然后被编译成 Python 模块。

  • 用 Python 代码调用原生 C/C++
  • 用静态类型声明把 Python 代码优化成 C 语言的性能

静态类型是Cython这个翻译器产生优化的C语言代码的主要特征

注意:这样做代码会变得啰嗦,降低维护性和可读性。因此通常不推荐。

  • 可以使用所有的C类型
  • Cython可以对变量赋值自动进行类型转换
  • 当面对Python的任意长度整数时,如果转换成C类型出现了栈溢出,Python的溢出错误就会产生。

1. 安装

1
pip install cython

2. 建立 Cython 模块

(1) 用 Cython 把 .pyx 文件编译成 .c 文件
(2) .c 文件被 gcc 编译成 .so 库,这个库之后可以导入 Python

例子1: 建立 Cython 模块时编译代码的几种方法

  • my_pyx_module.pyx
1
2
3
4
# cython: language_level=3

def join_n_print(parts):
print("".join(parts))

(1) 用 pyximport

用 pyximport,像导入 .py 文件一样导入 .pyx 直接使用

  • my_test.py
1
2
3
4
5
6
# cython: language_level=3

import pyximport
pyximport.install()
from my_pyx_module import join_n_print
join_n_print(["This", "is", "a", "test"])

(2) cython 命令

运行 cython 命令将 .pyx 文件编译成 .c 文件。然后用 gcc 编译成库文件

1
cython my_pyx_module.pyx

生成 my_pyx_module.c,然后编译为库文件

1
gcc -shared -o my_pyx_module.so -fPIC my_pyx_module.c

生成 my_pyx_module.so,然后就可以在 python 文件中 import my_pyx_module 了。

(3) distutils

创建 distutils 配置文件,它是创建其它模块的工具,可以生成自定义的 C 语言编译文件

  • setup.py
1
2
3
4
5
6
from distutils.core import setup
from Cython.Build import cythonize

setup(name="Test apple"
,ext_modules=cythonize("my_pyx_module.pyx")
)

在上面的代码中,要被导出的代码在 .pyx 文件中。

setup.py 文件可以通过不同的参数调用 setup 函数。最后,它会调用 test.py 文件,文件中导入并使用了库文件。

编译命令如下

1
python setup.py build_ext -inplace

生成的结果如下,除了翻译(Cython化)代码,且用 gcc 把代码编译成库文件 my_pyx_module.cpython-36m-x86_64-linux-gnu.so。

1
2
3
4
5
6
7
8
9
Compiling my_pyx_module.pyx because it changed.                                                                                
[1/1] Cythonizing my_pyx_module.pyx
running build_ext
building 'my_pyx_module' extension
creating build
creating build/temp.linux-x86_64-3.6
gcc -pthread -B /home/ppp/anaconda3/envs/python-3.6/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wal
l -Wstrict-prototypes -fPIC -I/home/ppp/anaconda3/envs/python-3.6/include/python3.6m -c my_pyx_module.c -o build/temp.linux-x86_64-3.6/my_pyx_module.o
gcc -pthread -shared -B /home/ppp/anaconda3/envs/python-3.6/compiler_compat -L/home/ppp/anaconda3/envs/python-3.6/lib -Wl,-rpa$h=/home/ppp/anaconda3/envs/python-3.6/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.6/my_pyx_module.o -o /h$me/ppp/codes/python/Mastering-Python-High-Performance/chapter 6/test/my_pyx_module.cpython-36m-x86_64-linux-gnu.so

之后就可以在 python 代码中直接 import my_pyx_module 了。

例子2: Cython 的定义文件(pxd)和实现文件(pyx)

面对复杂的情况时,Cython通常都需要导入两类文件。

  • 定义文件:文件扩展名.pxd,是其他Cython文件要使用的变量、类型、函数名称的C语言声明。
  • 实现文件:文件扩展名.pyx,包括在.pxd文件中已经定义好的函数实现

定义文件中通常包括C类型声明、外部C函数或变量声明,以及模块中定义的C函数声明
定义文件不包含任何C或Python函数的实现,也不包含任何Python类的定义或可执行代码行。

以下例子是文档 Sharing Declarations Between Cython Modules 的例子。

文件 dishes.pxd:

1
2
3
4
5
6
cdef enum otherstuff:
sausage, eggs, lettuce

cdef struct spamdish:
int oz_of_spam
otherstuff filler

文件 restaurant.pyx

1
2
3
4
5
6
7
8
9
10
11
12
13
# cython: language_level=3

cimport dishes
from dishes cimport spamdish

cdef void prepare(spamdish *d):
d.oz_of_spam = 42
d.filler = dishes.sausage

def serve():
cdef spamdish d
prepare(&d)
print("{} oz spam, filler no. {}".format(d.oz_of_spam, d.filler))

之后可以从三种编译 pyx 的方法中选一种,然后 import restaurant 并调用 serve(),可以输出结果如下:

1
42 oz spam, filler no. 0
  • 默认情况下,当运行cimport时,它会在搜索路径中查找同名模块的modulename.pxd文件。
  • 无论定义文件何时改变,导入的文件都需要重新编译。Cythin.Build.cythonize 功能可以解决这个问题。

(3) cython 中定义 c 语言的变量, 类型 — cdef

cdef 可以用于声明 C 语言的变量(local/module-level)

1
2
cdef int i, j, k
cdef float f, g[42], *h

cdef 可以用于声明 C 语言的struct, unionenum,相当于定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cdef struct Grail:
int age
float volume

cdef union Food:
char *spam
float *eggs

cdef enum CheeseType:
cheddar, edam,
camembert

cdef enum CheeseState:
hard = 1
soft = 2
runny = 3

cdef 可以用于定义常量, 可以用匿名 enum 实现

1
2
cdef enum:
tons_of_spam = 3

cdef 可以用于定义函数

1
2
cdef int eggs(unsigned long l, float f):
...

cdef 可以用于定义类,然后将其变成 Extension Types,它的行为与 Python class 很接近。

1
2
3
4
5
6
7
8
9
cdef class Shrubbery:
cdef int width, height

def __init__(self, w, h):
self.width = w
self.height = h

def describe(self):
print("This shrubbery is {} by {} cubits.".format(self.width, self.height))

关于在 Cython 中定义和类型,参考 Language Basics


3. 调用外部的 C 语言

(1) 在 Cython(.pyx) 中使用定义在头文件(.h) 中的变量

头文件 spam.h

1
2
3
4
5
6
7
8
#include <stdio.h>

int spam_counter = 10;

void order_spam(int tons)
{
printf("%d\n", spam_counter + tons);
}

Cython 文件 my.pyx

1
2
3
4
5
6
7
8
# cython: language_level=3

cdef extern from "spam.h":
int spam_counter;
void order_spam(int tons);

def func(n):
order_spam(spam_counter + n)

test.py

1
2
3
import my

my.func(2)

cdef extern from "spam.h" 的作用:

  • 它让Cython知道如何在生成的C语言代码中放入#include语句来引用我们需要使用的库。
  • 它会防止Cython为这段代码中的声明生成任何C代码。
  • 它会把代码中的所有声明看成cdef extern,表示那些声明是定义在其他地方。

Cython在任何时刻都不会读取头文件的内容。所以你还需要把头文件的内容进行重新声明。你只需要重新你需要的那些,不需要关心代码中的其他内容。例如,如果你在头文件中声明了一个成员很多的大结构体,你只需要重新声明你需要使用的成员。在编译时,C编译器会使用完整的结构体源代码。

(2) 在 Cython 中使用在头文件声明, C 文件中定义的 C 变量或函数

spam.h

1
2
3
int extern spam_counter;

void order_spam(int tons);

spam.c

1
2
3
4
5
6
7
8
9
#include "spam.h"
#include <stdio.h>

int spam_counter = 10;

void order_spam(int tons)
{
printf("%d\n", tons);
}

head.pxd:

1
2
3
cdef extern from "spam.c":
int spam_counter;
void order_spam(int tons);

注:上面这部分内容也可以直接放到 my.pyx 中

my.pyx

1
2
3
4
5
6
# cython: language_level=3

from head cimport spam_counter, order_spam

def func(n):
order_spam(spam_counter + n)

test.py

1
2
3
import my

my.func(2)

(3) cython 中调用 c 标准库函数 — cimport

与 Python 一样,Cython 也可以调用已经编译的 C 函数与 C 语言交互。

对于某些标准 C 库,直接 cimport 即可。

例子: 在 cython 中使用标准 C 库

在 cython 中调用 math 的 sin 和 stdlib 的 atoi

my.pxd 代码如下

1
2
3
4
5
6
7
8
9
10
# cython: language_level=3

from libc.math cimport sin
from libc.stdlib cimport atoi

def f(x):
return sin(x * x)

def my_atoi(s):
return atoi(s)

setup.py 代码(直接用 cython 命令也可以)

1
2
3
4
5
6
7
8
from distutils.core import setup, Extension
from Cython.Build import cythonize

extension = Extension("my", sources=["my.pyx"])

setup(name="Test apple"
,ext_modules=cythonize([extension])
)

在 python 中调用 f

1
2
3
4
5
6
7
8
9
import my

s = "9872"
d = my.my_atoi(bytes(s, "utf8"))
print(d)

x = 9
y = my.f(x)
print(y)

关于调用 C 语言的更多内容,参考Interfacing with External C Code


Share