原创:hxj7
本文是笔者第一次使用cython的一个小结
笔者最近参与了一个项目,其目的是提升一个python程序的运行速度。其中一个手段就是利用cython来优化原来的python代码。笔者之前没有接触过cython,所以这次属于在实践中学习新知识。
现在项目告一段落,所以笔者对自己使用cython的经验做一个小结,以便将来参考。文章较长,分为以下几个小节:
- 对cython的基本认识
- 使用cython所需准备的知识和技能储备
- cython的安装
- cython的语法和文件
- cython代码的编译
- cython代码编译后的使用
- 提升效率:将代码直接复制到.pyx文件中
- 提升效率:在cython中加上类型声明
-
提升效率:在cython中直接使用c代码
- 9.1 使用c/c++的标准库
- 9.2 使用c/c++的第三方库或者自编的代码
- 9.3 用c标准库函数提升效率的一个例子
- cython作为扩展被打包
- 总结
- 参考资料
<a id="1">1. 对cython的基本认识</a>
笔者对cython的认识是,首先它是一种编程语言;作为连接python和c/c++的工具,常见用途是利用c/c++的一些特性来提升python代码的运行效率,主要通过两个途径:
- python一般是没有类型声明的(最起码python2.x中没有见过),而在cython中可以指定对象的类型然后进行编译;
- 在cython中直接将包装好的c/c++代码拿来用(比如c中自定义的一些数据结构或者一些已经优化过的库函数)
<a id="2">2. 使用cython所需准备的知识和技能储备</a>
首先当然要掌握python的语法;除此之外,如上一小节所说,cython中会直接或者间接用到c/c++的代码,所以掌握一些c/c++的基本语法是必要的;其次,由于cython的代码也要进行编译,所以也要掌握一些关于编译(比如常见的gcc编译)的一些知识和用法。
<a id="3">3. cython的安装</a>
pip install Cython
为了说明的方便,新建一个文件夹demo/cy
,下文所涉及到的文件都放在这个文件夹下。
<a id="4">4. cython的语法和文件</a>
cython的语法大体上与python相同,但也有其特有的一些语法(具体可参考文末链接)。其代码一般都存放于.pyx
和.pxd
文件中。.pyx和.pxd文件分别类似于c语言中的.c和.h文件,即在.pyx中存放着一些变量、结构体或函数等对象的实现,如果这些对象想被其它.pyx文件使用,就得将它们定义在.pxd文件中。
cython中的函数可以被def
,cdef
,cpdef
修饰。一般来说,只有 def 或者 cpdef 修饰的函数才能通过import
语句直接被python调用;而 cdef 或者 cpdef 修饰的函数可以被其它.pyx文件通过cimport
的语句调用。
一个简单的例子如下:
#cy_utils.pyx
from math import log
def logsum(x, y):
return log(x + y)
看起来就像一般的python代码,只不过文件的后缀名是.pyx。当然,我们也可以定义一个 cdef 修饰的函数:
#cy_utils.pyx
from math import log
def logsum(x, y):
return log(x + y)
cdef double logsum2(double x, double y):
return log(x + y)
一般用 cdef 修饰的函数其参数和返回值都要求指定明确的类型,比如double。上面的例子中,logsum函数可以通过 import 语句被python代码直接调用(因为是被 def 修饰),而logsum2函数不可以(因为是被 cdef 修饰)。
<a id="5">5. cython代码的编译</a>
cython项目的构建从编写.pyx和.pxd文件开始,编写完成后有两个选择:一是先将cython代码编译,生成.so
文件,可供python调用;二是如果python项目需要打包的话,可以将cython代码作为扩展进行编译。无论哪种方式,cython都会被编译,而它的编译一般是通过编写setup.py
文件实现的。
setup.py既可以用于编译cython,也可以用于打包/安装python代码。setup.py的最核心功能是setup()
函数实现的,所以最重要的是为setup()函数选择合适的参数。对于编译cython代码而言,setup()函数的主要参数是Extension
,有两个选择,通过setuptools
模块导入的setuptools.extension.Extension,或者通过distutils
模块导入的distutils.extension.Extension,优先选择前者,因为前者是后者的增强版。
一个简单的例子是对上一小节中的cy_utils.pyx文件进行编译:
#setup.py
from setuptools import setup
from setuptools.extension import Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize([
Extension(
name='cy_utils', # if use path, seperate with '.'.
sources=['cy_utils.pyx'], # like .c files in c/c++.
language='c', # c or c++.
include_dirs=[], # like -I option in gcc.
library_dirs=[], # like -L option in gcc.
libraries=[], # like -l option in gcc.
extra_compile_args=[], # extra compile args passed to gcc.
extra_link_args=[] # extra link args passed to gcc.
)
])
)
此时,文件夹下有两个文件:
运行下面的命令可以对cy_utils.pyx进行编译并生成相应的.so文件。
python setup.py build_ext --inplace
可以看到文件夹下多了一个cy_utils.so文件:
<a id="6">6. cython代码编译后的使用</a>
那么这个编译好的cy_utils.so文件有什么用呢?简单来说,这个.so文件就相当于一个模块,可以被其它的python文件导入。比如:
#test.py
from cy_utils import logsum
def main(x, y):
print("res = %.6f" % logsum(x, y))
if __name__ == "__main__":
x, y = 3.1, 5.2
main(x, y)
运行这个测试文件test.py:
python test.py
结果如下:
至此,一个简单但是完整的cython项目就完成了。此时,文件夹里包括了cy_utils.pyx源文件,编译.pyx文件用的setup.py文件,编译好的cy_utils.so文件以及一个测试用的test.py文件。
<a id="7">7. 提升效率:将代码直接复制到.pyx文件中</a>
上面几个小节介绍了如何编写并编译简单的cython代码。与纯python代码相比,利用cython真的能提升运行效率吗?我们会从三个方面进行测试:
- 原来的函数等python代码不做修改,直接复制到.pyx文件中
- 在cython中加上类型声明
- 在cython中直接使用c代码
首先我们来看第一点,将代码直接复制到.pyx文件中。还是以上面的logsum函数为例,假设原来在py_utils.py文件中有一个logsum函数,现在我们将该函数复制到cy_utils.pyx文件中,然后在测试文件test.py中比较这两个函数的运行效率。
py_utils.py文件:
#py_utils.py
from math import log
def logsum(x, y):
return log(x + y)
cy_utils.pyx文件
#cy_utils.pyx
from math import log
def logsum(x, y):
return log(x + y)
setup.py内容不变。运行python setup.py build_ext --inplace
得到编译后的cy_utils.so文件。
test.py如下:
#test.py
from py_utils import logsum as py_logsum
from cy_utils import logsum as cy_logsum
import time
# a rough estimation of running time of certain function.
def run_time(x, y, rep, func, func_name):
total_time = 0.0
for i in range(5):
start_time = time.time()
for _ in range(rep):
res = func(x, y)
total_time += time.time() - start_time
stime = total_time / 5.0
print("%s: res = %.6f, time = %.2f" % (func_name, res, stime))
def main(x, y, rep):
run_time(x, y, rep, py_logsum, "py_logsum")
run_time(x, y, rep, cy_logsum, "cy_logsum")
if __name__ == "__main__":
main(3.1, 5.2, 10000000)
test.py的运行结果如下:
可以看出,没有改变logsum函数代码的前提下,将其直接从.py文件复制到.pyx文件并编译后也可以稍稍提高运行效率。
<a id="8">8. 提升效率:在cython中加上类型声明</a>
如上文所说,python一般是没有类型声明的,所以如果在cython中预先指定对象的类型,类似c/c++中的静态类型声明,是有可能提升运行效率的。
<a id="801">8.1 一次失败的修改</a>
我们接着上面的例子,将cy_utils.pyx中的logsum函数加上类型声明,看看是否会提高运行效率。
cy_utils.pyx文件
#cy_utils.pyx
from math import log
def logsum(x, y):
return log(x + y)
def cy_logsum2(double x, double y):
return log(x + y)
由于cy_utils.pyx内容改变了,所以需要重新编译。再次运行python setup.py build_ext --inplace
生成.so文件。
py_utils.py内容不变。test.py文件修改如下:
#test.py
from py_utils import logsum as py_logsum
from cy_utils import logsum as cy_logsum
from cy_utils import cy_logsum2
import time
# a rough estimation of running time of certain function.
def run_time(x, y, rep, func, func_name):
total_time = 0.0
for i in range(5):
start_time = time.time()
for _ in range(rep):
res = func(x, y)
total_time += time.time() - start_time
stime = total_time / 5.0
print("%s: res = %.6f, time = %.2f" % (func_name, res, stime))
def main(x, y, rep):
run_time(x, y, rep, py_logsum, "py_logsum")
run_time(x, y, rep, cy_logsum, "cy_logsum")
run_time(x, y, rep, cy_logsum2, "cy_logsum2")
if __name__ == "__main__":
main(3.1, 5.2, 10000000)
运行test.py,得到如下结果:
可以看出在这个例子中,加上类型声明后运行效率不仅没有提升,反倒下降,甚至比纯python的代码还要慢。
<a id="802">8.2 一个成功的例子</a>
接着我们看一个修改自Cython官网的例子,同样是加上类型声明,速度有了明显提升:
py_utils.py文件
#py_utils.py
def py_integ(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
ds = a + i * dx
s += ds ** 2 - ds
return s * dx
cy_utils.pyx
#cy_utils.pyx
def cy_integ(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
ds = a + i * dx
s += ds ** 2 - ds
return s * dx
def cy_integ2(double a, double b, int N):
cdef int i
cdef double s, dx, ds
s = 0
dx = (b - a) / N
for i in range(N):
ds = a + i * dx
s += ds ** 2 - ds
return s * dx
可以看到,cy_integ2函数已经加上诸多类型声明。和上面一样,既然cy_utils.pyx内容有变动,就需要重新编译。
setup.py内容不变。test.py文件:
#test.py
from py_utils import py_integ
from cy_utils import cy_integ, cy_integ2
import time
# a rough estimation of running time of certain function.
def run_time(x, y, N, rep, func, func_name):
total_time = 0.0
for i in range(5):
start_time = time.time()
for _ in range(rep):
res = func(x, y, N)
total_time += time.time() - start_time
stime = total_time / 5.0
print("%s: res = %.6f, time = %.2f" % (func_name, res, stime))
def main(x, y, N, rep):
run_time(x, y, N, rep, py_integ, "py_integ")
run_time(x, y, N, rep, cy_integ, "cy_integ")
run_time(x, y, N, rep, cy_integ2, "cy_integ2")
if __name__ == "__main__":
main(3.1, 5.2, 100, 100000)
运行test.py结果如下:
可以看出,加上类型声明后cy_integ2函数的速度是cy_integ的大概10倍。其奥秘就是两个 cdef 语句。尤其是cy_integ2中的
cdef int i
这一句让 i 的循环具有c语言中循环的速度。
<a id="803">8.3 小结</a>
从上面两个例子中可以看出,加上类型声明不总是会提升效率,需要根据具体情况来决定是否使用类型声明。
<a id="9">9. 提升效率:在cython中直接使用c代码</a>
c/c++的很多标准库或者成熟的第三方库都是高性能的,所以如果能在cython中直接使用这些库函数是很酷的。
<a id="901">9.1 使用c/c++的标准库</a>
好在cython自身就已经将c/c++中的很多库函数包装好了,可以很方便地调用。比如,cython中的libc模块就包装了很多c的标准库,这些标准库都被包装到对应的.pxd文件中:
我们可以像这样在cython中调用 stdio.h 中的 printf 函数,和python的语法很相似,只不过是用 cimport 代替 import。
#cy_utils.pyx
from libc.stdio cimport printf
printf("hello world\n")
<a id="902">9.2 使用c/c++的第三方库或者自编的代码</a>
如果想在cython中使用c/c++语言的第三方库或者自己写的c/c++代码,也很方便。假设我们有一个自己写的c4cy.h文件,里面有一个c_max函数:
c4cy.h
//c4cy.h
double c_max(double x, double y) {
return x > y ? x : y;
}
那么在cython中想引用c_max函数的话,可以在.pxd文件中用cdef extern from
实现:
#cy_include.pxd
cdef extern from "c4cy.h":
double c_max(double x, double y)
在.pxd文件中声明了c_max函数后,就可以在.pyx文件中调用该函数了,注意这里要用 cimport 来导入:
#cy_utils.pyx
from cy_include cimport c_max
def max3(double a, double b, double c):
return c_max(c_max(a, b), c)
<a id="903">9.3 用c标准库函数提升效率的一个例子</a>
我们接着开始的 logsum 函数的例子,该函数内部使用了python中math模块的log函数;其实,python中的 numpy 模块中的 log 函数也经常被使用;此外,c语言标准库 <math.h> 中也有一个 log 函数。所以,接下来,我们就比较一下这三个 log 函数的运行效率。
py_utils.py
#py_utils.py
from math import log as mt_log
def py_logsum(x, y):
return mt_log(x + y)
cy_utils.pyx
#cy_utils.pyx
from math import log as mt_log
from numpy import log as np_log
from libc.math cimport log as c_log
def cy_logsum(x, y):
return mt_log(x + y)
def np_logsum(x, y):
return np_log(x + y)
def c_logsum(x, y):
return c_log(x + y)
当然要重新编译cy_utils.pyx。
测试脚本test.py
#test.py
from py_utils import py_logsum
from cy_utils import cy_logsum, np_logsum, c_logsum
import time
# a rough estimation of running time of certain function.
def run_time(x, y, rep, func, func_name):
total_time = 0.0
for i in range(5):
start_time = time.time()
for _ in range(rep):
res = func(x, y)
total_time += time.time() - start_time
stime = total_time / 5.0
print("%s: res = %.6f, time = %.2f" % (func_name, res, stime))
def main(x, y, rep):
run_time(x, y, rep, py_logsum, "py_logsum")
run_time(x, y, rep, cy_logsum, "cy_logsum")
run_time(x, y, rep, np_logsum, "np_logsum")
run_time(x, y, rep, c_logsum, "c_logsum")
if __name__ == "__main__":
main(3.1, 5.2, 10000000)
结果如下:
从中可以看出,cy_utils.pyx中的三个log函数运行速度由快到慢依次是:c_log > mt_log > np_log。也就是说,在上面三个版本的log函数中,c版本的是最快的,而numpy版本的没有math模块版本中的快。
<a id="10">10. cython作为扩展被打包</a>
上面的例子中,我们都是将cython文件编译后供python脚本调用。在实际应用中经常需要发布python项目,如果项目中包含了cython代码,那么可以在setup()函数中将cython代码作为扩展和python代码一起打包。
仍以上一小节的 logsum 函数的例子来说明,我们可以这样修改setup.py来将cy_utils.pyx作为test的扩展:
#setup.py
from setuptools import setup, find_packages
from setuptools.extension import Extension
from Cython.Build import cythonize
reqs = ['numpy>=1.9.0', 'cython>=0.29.16']
setup(
name = "test",
version = "0.0.1",
packages=find_packages(),
install_requires=reqs,
ext_modules=cythonize([
Extension(
name='cy_utils', # if use path, seperate with '.'.
sources=['cy_utils.pyx'], # like .c files in c/c++.
language='c', # c or c++.
include_dirs=[], # like -I option in gcc.
library_dirs=[], # like -L option in gcc.
libraries=[], # like -l option in gcc.
extra_compile_args=[], # extra compile args passed to gcc.
extra_link_args=[] # extra link args passed to gcc.
)
])
)
<a id="11">11. 总结</a>
本次对cython项目经验的整理到这里就告一段落了。总的来说,
- cython的语法和python很相似,其基础用法学起来比较快,但是一些高级特性,如MemoryView,不易掌握。
- 利用cython来提升python代码的速度需要根据实际的情况灵活使用,否则可能会事倍功半。
- 为了编译cython代码,需要熟练掌握setup.py的编写,尤其是要熟悉Extension中的参数选择。
最后,本文所用例子的测试环境:
- System: Linux version 4.4.0-18362-Microsoft (gcc version 5.4.0)
- python: 2.7.17
- numpy: 1.16.6
- Cython: 0.29.16
<a id="12">12. 参考资料</a>
- 首先是Cython官方的wiki,非常好的理论学习资料:https://cython.readthedocs.io/en/latest/index.html
- Cython的github地址:https://github.com/cython/cython
其中可以着重看一下对c/c++标准库、numpy的包装:https://github.com/cython/cython/tree/master/Cython/Includes - pysam中包含了许多cython代码,是非常好的学习参考:https://github.com/pysam-developers/pysam/tree/74fa4ef2e21e0a02e2165e934d214eb772cf5bb6/pysam
以及所使用的setup.py:https://github.com/pysam-developers/pysam/blob/74fa4ef2e21e0a02e2165e934d214eb772cf5bb6/setup.py
基本上常用的cython知识和技巧都可以从中学习到。 - 最后是一个和MemoryView相关的很有意思的讨论贴:https://stackoverflow.com/questions/18462785/what-is-the-recommended-way-of-allocating-memory-for-a-typed-memory-view
(公众号:生信了)
网友评论