as 怎么将多个cpp文件代码编译成so_加速你的PY代码,让我们来简单上手一波Cython呀~...

ded88246f9aa4fd003160ec8e34e6fa0.png

原文链接

加速你的PY代码,让我们来简单上手一波Cython呀~​mp.weixin.qq.com
908083ddc7bd7772c87bb5ae50a39441.png

导语

众所周知,Python是一种非常简单易上手的胶水语言(胶水语言的意思就是用来连接软件组件的程序设计语言,通常是脚本语言)。尽管近年来Python越来越火,也被各种吹捧,但它的执行速度始终逃不出被人所诟病的窘境。不过好在目前已经有不少较为成熟的解决方案来为Python提速,今天我们就来简单介绍并上手一下其中一种非常不错的解决方案,也就是Cython。废话不多说,让我们愉快地开始吧~

简介介绍


这里我们先来简单介绍一下Cython吧。显然,Cython也是一种编程语言,按照官方文档里的说法,它可以通过类似Python的语法来编写可以被Python调用的C扩展。从而,在保留Python快速开发的优点的同时,可以提升Python速度并方便地调用外部C库。

为了证明我没有在胡说八道,还是把文档里对应的段落截出来吧:

65a79695616e71daa15b889e9b11bb67.png

总结一下,利用Cython,我们可以:

  • 利用Python的语法实现Python和C/C++的混合编程,从而可以提升Python的执行效率;
  • 调用C/C++代码。

最后,扫个盲,请注意Cython≠CPython。CPython的意思是,用C语言开发的Python解释器。换句话说,就是Python只是一种编程语言,具体实现的时候,如果用C来实现,那就叫CPython,如果用Java来实现,那就叫Jython,如果用C#来实现,那就叫IronPython,如果用Python自己来实现,那就叫PyPy。

OK,大概了解了Cython是啥之后,接下来让我们来简单上手一波吧~

简单上手

0.环境声明

懒得连Linux了,所以本文中所有例子的运行环境为:

  • 操作系统: windows10
  • Python版本: 3.6.4

1.安装Cython库

只需要在命令行执行如下命令就OK啦:

pip install cython

当然对于Anaconda用户,你也可以执行如下命令安装:

conda install cython

2.写两段Python代码并测速

这里,我们先用Python来写两段简单的代码,并测试一下它的运行速度。第一段代码是:

# fibonacci.py
def fib(n):
  if not isinstance(n, int):
    raise ValueError('n is incorrect')
  if n <= 1: return n
  return fib(n-1) + fib(n-2)

显然,这段纯Python代码是用来计算斐波那契数列的,我们将它放在一个名为fibonacci.py的文件中。让我们来执行并测试一下它的运行时间吧:

db6e480f49a56836a55e14fa50c189ea.png

接下来,我们再来写一段代码:

# matrixdot.py
import numpy as np

def dot(m1, m2):
  if m1.shape[1] != m2.shape[0]:
    raise ValueError('m1 and m2 dimension mismatch')
  r = np.zeros((m1.shape[0], m2.shape[1]), dtype=np.float32)
  for i in range(m1.shape[0]):
    for j in range(m2.shape[1]):
      s = 0
      for k in range(m1.shape[1]):
        s += m1[i, k] * m2[k, j]
      r[i, j] = s
  return r

显然,这是一段用来计算矩阵乘法的代码,我们将它放在matrixdot.py文件中。让我们也来测试一下它的执行时间吧:

f75fea71c11ae4489fbf6e46cf739467.png

3.简单的Cython加速

现在,我们来试试Cython是否可以有效地加速上面两个程序。对于第一段代码,我们直接新建一个fibonacci.pyx文件,里面的内容为:

# fibonacci.pyx
def fib(n):
  if not isinstance(n, int):
    raise ValueError('n is incorrect')
  if n <= 1: return n
  return fib(n-1) + fib(n-2)

fibonacci.pyfibonacci.pyx中的内容完全一致。唯一不同的只是程序的扩展名发生了改变,这是因为Cython程序的扩展名是.pyx。那么如何来调用这个Cython程序呢?一般地,其流程为:

  • 编译Cython代码,并生成对应的动态链接库;
  • Python解释器载入动态链接库。

为了完成第一步,我们需要写一个setup_fib.py程序:

# setup_fib.py
from Cython.Build import cythonize
from distutils.core import setup, Extension

setup(
  ext_modules=cythonize(Extension('fibonacci_cython',
                  sources=['fibonacci.pyx'],
                  language='c',
                  include_dirs=[],
                  library_dirs=[],
                  libraries=[],
                  extra_compile_args=[],
                  extra_link_args=[]))
)

简单解释一下:

(1) fibonacci_cython
生成的动态链接库名字。
(2) sources
需要包括的.pyx文件以及.c/.cpp文件。
(3) language
默认是c, 当然也可以是c++。
(4) include_dirs
相当于gcc的-I参数。
(5) library_dirs
相当于gcc的-L参数。
(6) libraries
相当于gcc的-l参数。
(7) extra_compile_args
相当于传给gcc的额外编译参数。
(8) extra_link_args
相当于传给gcc的额外链接参数。

想要进一步了解的可以阅读这个:

https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html

然后在终端执行如下命令把Cython程序编译成动态链接库:

python setup_fib.py build_ext --inplace

不出意外的话,可以看到当前目录多出来了两个文件:

1fd0deadae910d4c3ddc04afd78775dd.png

上面那个红框是生成的C程序,下面那个是编译好了的动态链接库。换句话说,Cython编译器先把Cython代码编译成调用了Python源码的C/C++代码,然后再将生成的代码编译成动态链接库。

注意,我们这里使用的是Windows系统,对于Mac/Linux系统,生成的动态链接库扩展名为.so。

让我们再来测试一下代码的执行时间:

0f9837a953e1c2268594b637493250d8.png

可以发现,我们在没有修改任何代码的情况下,仅仅将其编译成动态链接库,我们的代码就获得了5倍以上的提速。

利用相同的流程再来试试另外一段代码,新建matrixdot.pyx,内容与之前matrixdot.py一致。然后写一个setup_dot.py脚本:

# setup_dot.py
import numpy
from Cython.Build import cythonize
from distutils.core import setup, Extension

setup(
  ext_modules=cythonize(Extension('matrixdot_cython',
                  sources=['matrixdot.pyx'],
                  language='c',
                  include_dirs=[numpy.get_include()],
                  library_dirs=[],
                  libraries=[],
                  extra_compile_args=[],
                  extra_link_args=[]))
)

编译后调用测试一下时间:

9f0b808b99ae0dcc0488369dfc7bbff9.png

可以发现,对于稍微复杂一些的第二个程序(这里的复杂主要是指程序中的变量更多了),虽然执行时间有所下降,但效果并不显著(从1.18s降到了1.01s)。

4.添加类型注释

现在,我们来修改一下之前的fibonacci.pyx文件,为里面的变量添加类型注释:

# fibonacci.pyx
import cython

@cython.boundscheck(False)
@cython.wraparound(False)
cdef int _fib(int n):
  if not isinstance(n, int):
    raise ValueError('n is incorrect')
  if n <= 1: return n
  return fib(n-1) + fib(n-2)

def fib(n):
  return _fib(n)

简单解释一下:

(1) @cython.boundscheck(False)和cython.wraparound(False)
关闭Cython的边界检查。
(2) cdef
定义函数, 并且可以给所有参数以及返回值指定类型。
(3) def
因为在python程序中, 我们是看不到cdef函数的, 所以我们这里要
再用def定义一个fib函数, 来调用cdef的_fib函数。

我们再来编译执行一下这个程序:

b3d95ec02f9c5ff9e36f8c81800d2be5.png

可以发现,对于这个简单的程序(这里的简单是指程序中的变量相对较少),添加了类型注释之后,执行的效率提升并不明显,但还是有提升的。接着再来修改我们的第二个程序:

# matrixdot.py
import numpy as np
cimport cython
cimport numpy as np

@cython.boundscheck(False)
@cython.wraparound(False)
cdef np.ndarray[np.float32_t, ndim=2] _dot(np.ndarray[np.float32_t, ndim=2] m1, np.ndarray[np.float32_t, ndim=2] m2):
  cdef np.ndarray[np.float32_t, ndim=2] r
  cdef int i, j, k
  cdef np.float32_t s
  if m1.shape[1] != m2.shape[0]:
    raise ValueError('m1 and m2 dimension mismatch')
  r = np.zeros((m1.shape[0], m2.shape[1]), dtype=np.float32)
  for i in range(m1.shape[0]):
    for j in range(m2.shape[1]):
      s = 0
      for k in range(m1.shape[1]):
        s += m1[i, k] * m2[k, j]
      r[i, j] = s
  return r

def dot(m1, m2):
  return _dot(m1, m2)

简单解释一下之前没有的内容:

(1) cimport
用来引入.pxd文件(相当于c/c++中的头文件)的命令。
(2) 在函数内部, 我们可以使用cdef typename varname这样的语法
来声明变量。

编译执行一下看看:

a840dd92e695956b7e5afd4a150f2e35.png

哇,我们可以发现程序的执行效率大概提升了300倍!!!

于是我们可以得出一个结论,在Cython中,类型声明对于提升程序执行效率至关重要。

5.Cython分析工具

既然类型声明对于Cython效率至关重要,那么如何检查我们的程序有没有漏加类型声明呢?我们可以在命令行执行如下命令:

cython -a matrixdot.pyx

可以看到当前目录会生成一个.html文件:

2cfbe974fd8d476b05fc8ec6f5905e34.png

打开可以看到:

4a177758f24e840e14e5877e4678663b.png

这里黄色标出的部分就是程序中拖累Cython性能的部分。如果不加声明的话,代码就全黄了:

44a69ca76d88de4dc6714da3d81deed2.png

当然,我们没有必要绞尽脑汁地把所有黄色部分去掉。实际上,我们只需要保证核心代码部分执行速度足够快就行了。不然就谈不上最开始说的用Cython是为了兼顾开发效率和执行效率了。

6.直接调用C代码

最后再来补充一下简单的Cython调用C代码的例子。我们把斐波那契数列那个代码写成C的形式(在cfib.c文件中):

#include "stdio.h"

static int cfib(int n){
  if(n <= 1)
  {
    return n;
  }
  return cfib(n-1) + cfib(n-2);
}

修改一下fibonacci.pyx中的内容:

cdef extern from "cfib.c":
  int cfib(int n)

def fib(n):
  return cfib(n)

重新编译执行一下:

5acb5d0efd4c67eccba4da22943bcbd2.png

提升了多少倍我就不多说了,别老想着骗我写c/c++。

e2a4be660b5673ef22e62443f174cee6.png

今天就先这样呗,有空我们再来聊聊怎么进一步上手Cython吧。相关文件我就不提供了,反正正文里都贴出来了。

参考文献

[1]. https://zhuanlan.zhihu.com/p/24311879
[2]. https://github.com/cython/cython
[3]. http://docs.cython.org/en/latest/index.html
[4]. https://cython.readthedocs.io/en/latest/index.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值