Cython 三分钟入门:从 Python 到 C 扩展的性能优化实战

宋时安
宋时安
发布时间:2026-10-14 | 2人浏览过
原创

本文通过计算地球表面两点间距离的示例,演示如何使用 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 的前身,它们有类似的目标和语法。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表