前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >cython初体验

cython初体验

作者头像
一只羊
发布2020-06-16 17:53:34
1.2K0
发布2020-06-16 17:53:34
举报
文章被收录于专栏:生信了

本文是笔者第一次使用cython的一个小结

笔者最近参与了一个项目,其目的是提升一个python程序的运行速度。其中一个手段就是利用cython来优化原来的python代码。笔者之前没有接触过cython,所以这次属于在实践中学习新知识。

现在项目告一段落,所以笔者对自己使用cython的经验做一个小结,以便将来参考。文章较长,分为以下几个小节:

  1. 对cython的基本认识
  2. 使用cython所需准备的知识和技能储备
  3. cython的安装
  4. cython的语法和文件
  5. cython代码的编译
  6. cython代码编译后的使用
  7. 提升效率:将代码直接复制到.pyx文件中
  8. 提升效率:在cython中加上类型声明
    • 8.1 一次失败的修改
    • 8.2 一个成功的例子
    • 8.3 小结
  9. 提升效率:在cython中直接使用c代码
    • 9.1 使用c/c++的标准库
    • 9.2 使用c/c++的第三方库或者自编的代码
    • 9.3 用c标准库函数提升效率的一个例子
  10. cython作为扩展被打包
  11. 总结
  12. 参考资料
1. 对cython的基本认识

笔者对cython的认识是,首先它是一种编程语言;作为连接python和c/c++的工具,常见用途是利用c/c++的一些特性来提升python代码的运行效率,主要通过两个途径:

  • python一般是没有类型声明的(最起码python2.x中没有见过),而在cython中可以指定对象的类型然后进行编译;
  • 在cython中直接将包装好的c/c++代码拿来用(比如c中自定义的一些数据结构或者一些已经优化过的库函数)
2. 使用cython所需准备的知识和技能储备

首先当然要掌握python的语法;除此之外,如上一小节所说,cython中会直接或者间接用到c/c++的代码,所以掌握一些c/c++的基本语法是必要的;其次,由于cython的代码也要进行编译,所以也要掌握一些关于编译(比如常见的gcc编译)的一些知识和用法。

3. cython的安装
代码语言:javascript
复制
pip install Cython

为了说明的方便,新建一个文件夹demo/cy,下文所涉及到的文件都放在这个文件夹下。

4. cython的语法和文件

cython的语法大体上与python相同,但也有其特有的一些语法(具体可参考文末链接)。其代码一般都存放于.pyx.pxd文件中。.pyx和.pxd文件分别类似于c语言中的.c和.h文件,即在.pyx中存放着一些变量、结构体或函数等对象的实现,如果这些对象想被其它.pyx文件使用,就得将它们定义在.pxd文件中。

cython中的函数可以被defcdefcpdef修饰。一般来说,只有 def 或者 cpdef 修饰的函数才能通过import语句直接被python调用;而 cdef 或者 cpdef 修饰的函数可以被其它.pyx文件通过cimport的语句调用。

一个简单的例子如下:

代码语言:javascript
复制
#cy_utils.pyx
from math import log

def logsum(x, y):
  return log(x + y)

看起来就像一般的python代码,只不过文件的后缀名是.pyx。当然,我们也可以定义一个 cdef 修饰的函数:

代码语言:javascript
复制
#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 修饰)。

5. cython代码的编译

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文件进行编译:

代码语言:javascript
复制
#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文件。

代码语言:javascript
复制
python setup.py build_ext --inplace

可以看到文件夹下多了一个cy_utils.so文件:

6. cython代码编译后的使用

那么这个编译好的cy_utils.so文件有什么用呢?简单来说,这个.so文件就相当于一个模块,可以被其它的python文件导入。比如:

代码语言:javascript
复制
#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:

代码语言:javascript
复制
python test.py

结果如下:

至此,一个简单但是完整的cython项目就完成了。此时,文件夹里包括了cy_utils.pyx源文件,编译.pyx文件用的setup.py文件,编译好的cy_utils.so文件以及一个测试用的test.py文件。

7. 提升效率:将代码直接复制到.pyx文件中

上面几个小节介绍了如何编写并编译简单的cython代码。与纯python代码相比,利用cython真的能提升运行效率吗?我们会从三个方面进行测试:

  • 原来的函数等python代码不做修改,直接复制到.pyx文件中
  • 在cython中加上类型声明
  • 在cython中直接使用c代码

首先我们来看第一点,将代码直接复制到.pyx文件中。还是以上面的logsum函数为例,假设原来在py_utils.py文件中有一个logsum函数,现在我们将该函数复制到cy_utils.pyx文件中,然后在测试文件test.py中比较这两个函数的运行效率。

py_utils.py文件:

代码语言:javascript
复制
#py_utils.py
from math import log

def logsum(x, y):
  return log(x + y)

cy_utils.pyx文件

代码语言:javascript
复制
#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如下:

代码语言:javascript
复制
#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文件并编译后也可以稍稍提高运行效率。

8. 提升效率:在cython中加上类型声明

如上文所说,python一般是没有类型声明的,所以如果在cython中预先指定对象的类型,类似c/c++中的静态类型声明,是有可能提升运行效率的。

8.1 一次失败的修改

我们接着上面的例子,将cy_utils.pyx中的logsum函数加上类型声明,看看是否会提高运行效率。

cy_utils.pyx文件

代码语言:javascript
复制
#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文件修改如下:

代码语言:javascript
复制
#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的代码还要慢。

8.2 一个成功的例子

接着我们看一个修改自Cython官网的例子,同样是加上类型声明,速度有了明显提升:

py_utils.py文件

代码语言:javascript
复制
#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

代码语言:javascript
复制
#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文件:

代码语言:javascript
复制
#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语言中循环的速度。

8.3 小结

从上面两个例子中可以看出,加上类型声明不总是会提升效率,需要根据具体情况来决定是否使用类型声明。

9. 提升效率:在cython中直接使用c代码

c/c++的很多标准库或者成熟的第三方库都是高性能的,所以如果能在cython中直接使用这些库函数是很酷的。

9.1 使用c/c++的标准库

好在cython自身就已经将c/c++中的很多库函数包装好了,可以很方便地调用。比如,cython中的libc模块就包装了很多c的标准库,这些标准库都被包装到对应的.pxd文件中:

我们可以像这样在cython中调用 stdio.h 中的 printf 函数,和python的语法很相似,只不过是用 cimport 代替 import。

代码语言:javascript
复制
#cy_utils.pyx
from libc.stdio cimport printf
printf("hello world\n")
9.2 使用c/c++的第三方库或者自编的代码

如果想在cython中使用c/c++语言的第三方库或者自己写的c/c++代码,也很方便。假设我们有一个自己写的c4cy.h文件,里面有一个c_max函数:

c4cy.h

代码语言:javascript
复制
//c4cy.h
double c_max(double x, double y) {
  return x > y ? x : y;
}

那么在cython中想引用c_max函数的话,可以在.pxd文件中用cdef extern from实现:

代码语言:javascript
复制
#cy_include.pxd
cdef extern from "c4cy.h":
  double c_max(double x, double y)

在.pxd文件中声明了c_max函数后,就可以在.pyx文件中调用该函数了,注意这里要用 cimport 来导入:

代码语言:javascript
复制
#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)
9.3 用c标准库函数提升效率的一个例子

我们接着开始的 logsum 函数的例子,该函数内部使用了python中math模块的log函数;其实,python中的 numpy 模块中的 log 函数也经常被使用;此外,c语言标准库 <math.h> 中也有一个 log 函数。所以,接下来,我们就比较一下这三个 log 函数的运行效率。

py_utils.py

代码语言:javascript
复制
#py_utils.py
from math import log as mt_log

def py_logsum(x, y):
  return mt_log(x + y)

cy_utils.pyx

代码语言:javascript
复制
#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

代码语言:javascript
复制
#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模块版本中的快。

10. cython作为扩展被打包

上面的例子中,我们都是将cython文件编译后供python脚本调用。在实际应用中经常需要发布python项目,如果项目中包含了cython代码,那么可以在setup()函数中将cython代码作为扩展和python代码一起打包。

仍以上一小节的 logsum 函数的例子来说明,我们可以这样修改setup.py来将cy_utils.pyx作为test的扩展:

代码语言:javascript
复制
#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.
    )
  ])
)
11. 总结

本次对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
12. 参考资料
  1. 首先是Cython官方的wiki,非常好的理论学习资料:https://cython.readthedocs.io/en/latest/index.html
  2. Cython的github地址:https://github.com/cython/cython 其中可以着重看一下对c/c++标准库、numpy的包装:https://github.com/cython/cython/tree/master/Cython/Includes
  3. pysam中包含了许多cython代码,是非常好的学习参考:https://github.com/pysam-developers/pysam/tree/74fa4ef2e21e0a02e2165e934d214eb772cf5bb6/pysam 以及所使用的setup.py:https://github.com/pysam-developers/pysam/blob/74fa4ef2e21e0a02e2165e934d214eb772cf5bb6/setup.py 基本上常用的cython知识和技巧都可以从中学习到。
  4. 最后是一个和MemoryView相关的很有意思的讨论贴:https://stackoverflow.com/questions/18462785/what-is-the-recommended-way-of-allocating-memory-for-a-typed-memory-view
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信了 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 对cython的基本认识
  • 2. 使用cython所需准备的知识和技能储备
  • 3. cython的安装
  • 4. cython的语法和文件
  • 5. cython代码的编译
  • 6. cython代码编译后的使用
  • 7. 提升效率:将代码直接复制到.pyx文件中
  • 8. 提升效率:在cython中加上类型声明
    • 8.1 一次失败的修改
      • 8.2 一个成功的例子
        • 8.3 小结
        • 9. 提升效率:在cython中直接使用c代码
          • 9.1 使用c/c++的标准库
            • 9.2 使用c/c++的第三方库或者自编的代码
              • 9.3 用c标准库函数提升效率的一个例子
              • 10. cython作为扩展被打包
              • 11. 总结
                • 12. 参考资料
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档