美文网首页初见
cython初体验

cython初体验

作者: 生信了 | 来源:发表于2020-06-11 11:59 被阅读0次

    原创:hxj7

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

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

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

    1. 对cython的基本认识
    2. 使用cython所需准备的知识和技能储备
    3. cython的安装
    4. cython的语法和文件
    5. cython代码的编译
    6. cython代码编译后的使用
    7. 提升效率:将代码直接复制到.pyx文件中
    8. 提升效率:在cython中加上类型声明
    9. 提升效率:在cython中直接使用c代码
    10. cython作为扩展被打包
    11. 总结
    12. 参考资料
    <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中的函数可以被defcdefcpdef修饰。一般来说,只有 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.
        )
      ])
    )
    

    此时,文件夹下有两个文件:

    image
    运行下面的命令可以对cy_utils.pyx进行编译并生成相应的.so文件。
    python setup.py build_ext --inplace
    

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

    image
    <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
    

    结果如下:

    image
    至此,一个简单但是完整的cython项目就完成了。此时,文件夹里包括了cy_utils.pyx源文件,编译.pyx文件用的setup.py文件,编译好的cy_utils.so文件以及一个测试用的test.py文件。
    image
    <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的运行结果如下:

    image
    可以看出,没有改变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,得到如下结果:

    image
    可以看出在这个例子中,加上类型声明后运行效率不仅没有提升,反倒下降,甚至比纯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结果如下:

    image
    可以看出,加上类型声明后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文件中:

    image
    我们可以像这样在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)
    

    结果如下:

    image
    从中可以看出,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>
    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.pyhttps://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

    (公众号:生信了)

    相关文章

      网友评论

        本文标题:cython初体验

        本文链接:https://www.haomeiwen.com/subject/vjfbtktx.html