Cython 三分钟入门:从 Python 到 C 扩展的性能优化实战
本文通过计算地球表面两点间距离的示例,演示如何使用 Cython 将 Python 代码转换为 C 扩展以提升执行速度。文章详细讲解了类型声明、调用 C 标准库函数以及优化循环结构的具体步骤,展示了从纯 Python 到 Cython 优化的性能提升过程,并提供了编译指南与最佳实践建议。
作者:perrygeo 译者:赖勇浩(http://laiyonghao.com) 原文:http://www.perrygeo.net/wordpress/?p=116
Cython 简介与核心优势
我最钟情于 Python,因其代码既优雅又实用。然而,纯粹从执行速度来看,Python 往往比大多数其他语言慢。许多人认为速度与易用性是不可兼得的——编写 C 代码确实是一项痛苦的任务。Cython 旨在消除这种矛盾,让你同时拥有 Python 的语法便利和 C 的数据类型及函数优势——这两者都是各自领域中的佼佼者。请注意,我并非此领域的专家,以下是我初次使用 Cython 的真实体验笔记:
编辑说明:根据收到的反馈,大家似乎有些混淆——Cython 用于生成 C 扩展而非独立程序。所有的加速都是针对现有 Python 应用中的特定函数进行的。我们并没有用 C 或 Lisp 重写整个应用程序,也没有手写 C 扩展。只是采用一种简单的方法,将 C 的速度和 C 数据类型整合到 Python 函数中。
现在我们可以尝试加速下面的 `great_circle` 函数。该函数用于计算沿地球表面两点之间的距离:
p1.py
import math
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 = math.acos((math.cos(a)*math.cos(b)) +
(math.sin(a)*math.sin(b)*math.cos(theta)))
return radius*c
让我们调用该函数 50 万次并测定其耗时:
import timeit
lon1, lat1, lon2, lat2 = -72.345, 34.323, -61.823, 54.826
num = 500000
t = timeit.Timer("p1.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import p1")
print "Pure python function", t.timeit(num), "sec"
结果约为 2.2 秒。这太慢了!
基础类型声明与编译流程
让我们尝试用 Cython 快速改写它,看看是否有性能差异: c1.pyx
import math
def great_circle(float lon1,float lat1,float lon2,float lat2):
cdef float radius = 3956.0
cdef float pi = 3.14159265
cdef float x = pi/180.0
cdef float a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = math.acos((math.cos(a)*math.cos(b)) + (math.sin(a)*math.sin(b)*math.cos(theta)))
return radius*c
请注意,我们仍然 `import math`——Cython 允许你在一定程度上混合使用 Python 和 C 数据类型。类型转换是自动进行的,但并非没有代价。在这个例子中,我们只是定义了一个 Python 函数,声明其输入参数为浮点类型,并为所有变量声明了 C 浮点数据类型。计算部分仍然使用了 Python 的 `math` 模块。
现在我们需要将其转换为 C 代码,再编译为 Python 扩展。完成这一步最好的方法是编写一个名为 `setup.py` 的发布脚本。但是,为了了解其中的原理,我们现在采用手工方式:
# 这将创建一个 c1.c 文件 - 用于构建 python 扩展的 C 源代码
cython c1.pyx
# 编译目标文件
gcc -c -fPIC -I/usr/include/python2.5/ c1.c
# 链接到共享库
gcc -shared c1.o -o c1.so
现在你应该有一个 `c1.so`(或 `.dll`)文件,它可以被 Python `import`。现在运行一下:
t = timeit.Timer("c1.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import c1")
print "Cython function (still using python math)", t.timeit(num), "s
结果约为 1.8 秒。并没有我们一开始期望的那种巨大的性能提升。使用 Python 的 `math` 模块应该是瓶颈。现在让我们使用 C 标准库替代它:
调用 C 标准库优化数学计算
c2.pyx
cdef extern from "math.h":
float cosf(float theta)
float sinf(float theta)
float acosf(float theta)
def great_circle(float lon1,float lat1,float lon2,float lat2):
cdef float radius = 3956.0
cdef float pi = 3.14159265
cdef float x = pi/180.0
cdef float a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = acosf((cosf(a)*cosf(b)) + (sinf(a)*sinf(b)*cosf(theta)))
return radius*cec"
与 `import math` 相应,我们使用 `cdef extern` 的方式从指定头文件声明函数(在此即使用 C 标准库的 `math.h`)。我们替代了代价高昂的 Python 函数,然后建立新的共享库,并重新测试:
t = timeit.Timer("c2.great_circle(%f,%f,%f,%f)" % (lon1,lat1,lon2,lat2),
"import c2")
print "Cython function (using trig function from math.h)", t.timeit(num), "sec"
现在结果令人满意了吗?0.4 秒——比纯 Python 函数有 5 倍的速度增长。我们还有什么方法可以再提高速度?`c2.great_circle()` 仍是一个 Python 函数调用,这意味着它产生 Python API 的开销(构建参数元组等)。如果我们可以写一个纯粹的 C 函数的话,我们也许能够加快速度。
极致性能:纯 C 函数与循环优化
c3.pyx
cdef extern from "math.h":
float cosf(float theta)
float sinf(float theta)
float acosf(float theta)
cdef float _great_circle(float lon1,float lat1,float lon2,float lat2):
cdef float radius = 3956.0
cdef float pi = 3.14159265
cdef float x = pi/180.0
cdef float a,b,theta,c
a = (90.0-lat1)*(x)
b = (90.0-lat2)*(x)
theta = (lon2-lon1)*(x)
c = acosf((cosf(a)*cosf(b)) + (sinf(a)*sinf(b)*cosf(theta)))
return radius*c
def great_circle(float lon1,float lat1,float lon2,float lat2,int num):
cdef int i
cdef float x
for i from 0 < = i < num:
x = _great_circle(lon1,lat1,lon2,lat2)
return x
请注意,我们仍然有一个 Python 函数(`def`),它接受一个额外的参数 `num`。这个函数里的循环使用 `for i from 0 < = i < num:`,而不是更 Pythonic 但慢得多的 `for i in range(num):`。真正的计算工作是在 C 函数(`cdef`)中进行的,它返回 float 类型。这个版本只要 0.2 秒——比原先的 Python 函数速度提高 10 倍。
纯 C 对比验证与最佳实践建议
为了证明我们所做的优化已经足够,可以用纯 C 写一个小应用,然后测定时间:
#include <math .h>
#include <stdio .h>
#define NUM 500000
float great_circle(float lon1, float lat1, float lon2, float lat2){
float radius = 3956.0;
float pi = 3.14159265;
float x = pi/180.0;
float a,b,theta,c;
a = (90.0-lat1)*(x);
b = (90.0-lat2)*(x);
theta = (lon2-lon1)*(x);
c = acos((cos(a)*cos(b)) + (sin(a)*sin(b)*cos(theta)));
return radius*c;
}
int main() {
int i;
float x;
for (i=0; i < = NUM; i++)
x = great_circle(-72.345, 34.323, -61.823, 54.826);
printf("%f", x);
}
用 `gcc -lm -o ctest ctest.c` 编译它,测试用 `time ./ctest ...` 大约 0.2 秒。这使我有信心,我的 Cython 扩展相对于我的 C 代码也极有效率(这并不是说我的 C 编程能力很弱)。
能够用 Cython 优化多少性能通常取决于有多少循环、数字运算和 Python 函数调用,这些都会让程序变慢。已经有一些人报告说在某些案例上 100 至 1000 倍的速度提升。至于其他的任务,可能不会那么有用。在疯狂地用 Cython 重写 Python 代码之前,记住这一点:
"我们应该忘记小的效率,过早的优化是一切罪恶的根源,有 97% 的案例如此。"——Donald Knuth
换句话说,先用 Python 编写程序,然后看它是否能够满足需要。大多数情况下,它的性能已经足够好了……但有时候真的觉得慢了,那就使用分析器找到瓶颈函数,然后用 Cython 重写,很快就能够得到更高的性能。
外部链接 WorldMill(http://trac.gispython.org/projects/PCL/wiki/WorldMill)——由 Sean Gillies 用 Cython 编写的一个快速的,提供简洁的 python 接口的模块,封装了用以处理矢量地理空间数据的 libgdal 库。
编写更快的 Pyrex 代码(http://www.sagemath.org:9001/WritingFastPyrexCode)——Pyrex,是 Cython 的前身,它们有类似的目标和语法。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













