加速Python的几种方法

前言

注意以下内容均为本人主观操作感受,运行环境为Win10+python3.7_32位,仅供本人日后查阅

众所周知,python是一门优美而简洁的编程语言,其最大的不足在于运行速度过于感人,而C语言作为最接近底层的高级语言,速度快是他最大的优势。python被誉为胶水语言,应该有办法和C语言胶和起来,我查阅资料发现python调用C语言最普遍的方式大概有三种:

  1. 将C语言的函数编译成dll动态链接库,然后通过python的ctypes模块对dll进行调用。
  2. 借由cython先将python代码编译成C语言代码,然后再讲C代码编译成.pyd文件直接在python中导入进行调用,cython本身有一定语法,不过不会一般也不太影响使用。
  3. 使用Swig可以将C代码直接封装成python代码,过程略为复杂,优点是可以为多种语言提供接口。

以上几种方法的原理和过程下图很好的解释了:

图片来自网络

本文以斐波拉契函数为例,探究python和C的合璧之法。

import time

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
start=time.time()
print("计算结果:%d" % fib(40))
end=time.time()
print('python time cost:',end-start)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int fib(int n)
{
    if (n == 0)
    {
        return 0;
    }
    else if (n == 1)
    {
        return 1;
    }
    else
    {
        return fib(n - 1) + fib(n - 2);
    }
}

int main()
{
    clock_t start, end;
    start = clock();
    printf("计算结果:%d\n", fib(40));
    end = clock();
    printf("C cost time:%f", (double)(end - start) / CLOCKS_PER_SEC);
    system("pause");
    return 0;
}

通过运行上面的代码可以得到运行时间:

  • 纯python:

计算结果:102334155
python time cost: 87.49325394630432

  • 纯C语言:

计算结果:102334155
C cost time:0.863000

差不多差了两个数量级吧。。。

1. 通过ctypes调用dll

首先得先生成一个dll,你可以在ide里新建一个dll工程进行创建。更简单的做法是利用下面的gcc命令直接在cmd里进行编译,但前提是你已经装了Mingw或者并Mingw-w64且设置了环境变量,其中有个比较坑的地方就是你编译出来的dll位数要和你的python位数要匹配,像我笔记本上装的是Mingw-w64,而pyhton是32位的,32的python无法调用64位的dll,而这位Mingw-w64虽说同时支持64位和32位,但是前提是安装时勾选的是sjlj而不是seh,seh不支持32位!

用于编译dll的c语言代码只需给出函数就好了:

int fib(int n)
{
    if(n == 0)
    {
        return 0;
    }
    else if(n == 1)
    {
        return 1;
    }
    else
    {
        return fib(n-1) + fib(n-2);
    }
}

下面是cmd中将C原文件编译成dll的命令:

gcc -fPIC -shared name.c -o name.dll

如果是sjlj的Mingw-w64加上-m32可以编译出32位dll:

gcc -fPIC -shared -m32 name.c -o name.dll

然后在python中导入ctypes模块就可以调用dll了:

import ctypes
import time

start=time.time()
fibpy=ctypes.CDLL('fibpy.dll')
print("计算结果:%d" % fibpy.fib(40))
end=time.time()
print('dll time cost:',end-start)

ctype调用dll运行时间:

  • 计算结果:102334155
  • dll time cost: 0.9999649524688721

因为只有一个函数,还调用的是C语言的,所以速度和纯C差不多。

2. cython

先给出官方的一个教程:官方教程

使用cython首先要安装cython模块:

pip install Cython

然后你需要安装Mingw进行一番配置或者直接下载安装Visual Studio安装C++环境,因为我用的是Mingw-win64,所以自然就先抛弃后面那个巨无霸选用前者,一番尝试后发现还是Visual Studio真香。当然可能单纯的是因为Mingw-win64不支持而已,用Mingw的话应该是可以的(但愿)。

安装完环境后就可以直接把之前的纯python代码中的函数部分进行cython转换,不过需要现将其后缀名改为.pyx,然后需要再创建一个setup.py文件,其中写入代码:

from distutils.core import setup
from Cython.Build import cythonize
setup(
    ext_modules = cythonize("fibcy.pyx")
)

然后还是cmd中执行:

python setup.py build_ext --inplace

这时应该会生成一个.pyd文件,这个文件本质和前面的dll应该差不多,可以通过import在python中使用:

import time
import fibcy
    
start=time.time()
print("计算结果:%d" % fibcy.fib(40))
end=time.time()
print('cython time cost:',end-start)

运行结果:

  • 计算结果:102334155
  • cython time cost: 15.108871698379517

可以看到即使是在没有使用任何cython语法的情况下,cython的加速能力也是令人惊叹的,运行速度提高了一个数量级左右!

3. Swig

同样先给出一个教程参考:Swig教程

其实swig的原理和前面也差不多,但是过程更为复杂。首先当然要安装swig,下载解压后同样添加到系统环境变量。

然后要准备的有C语言源文件,一个.i文件,还有一个setup.py文件。

#include <stdio.h>

int fib(int n)
{
    if (n == 0)
    {
        return 0;
    }
    else if (n == 1)
    {
        return 1;
    }
    else
    {
        return fib(n - 1) + fib(n - 2);
    }
}
%module fib
%{
#include <stdio.h>
%}
extern int fib(int n);
from setuptools import setup,Extension
setup(  name='fib',
    version='1.0',
    ext_modules=[Extension('_fib', ['fib.c', 'fib.i'])]
    )

最后在命令行运行setup.py:

python setup.py build_ext --inplace

最后生成的文件中应该有一个python文件,可以直接进行调用,除此之外我发现还有一个.pyd文件,果然和前面的方法都差不多呢,导入运行方法也和之前相同。

运行结果:

  • 计算结果:102334155
  • swig time cost: 0.6825790405273438

居然比纯C语言还要快!大概是因为去掉了计时模块的缘故,可能python的计时模块比C的要快?结果还是很匪夷所思呢?

4. namba的@jit加速

namba加速不同于以上3种方法,上面3种方法多多少少会和C语言扯上点关系。numba则是直接在python源文件里通过加上一些代码直接运行并获得加速效果,本质原理也是使数据向静态靠拢,但是实现过程与上面3种方法相比更简单,可以参考官方入门教程

下面具体看一下实现过程:
首先肯定是要先安装numba包

pip install numba

然后只需在Python代码中导入numba并用jit修饰相应的函

import time
from numba import jit

@jit(nopython=True)
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
start=time.time()
print("计算结果:%d" % fib(40))
end=time.time()
print('numba@jit time cost:',end-start)

运行结果:

  • 计算结果:102334155
  • numba@jit time cost: 1.2832109928131104

加速效果可以说是相当惊人的,比cython要快,而且操作也特别简单,不用导来导去,和numpy相配合使用可以使运行速度得到极大的提升。

总结

方案运行时间
纯Python87.49325394630432
纯C语言0.863000
ctypes0.9999649524688721
cython15.108871698379517
swig0.6825790405273438
numba1.2832109928131104

python结合c语言的三种方法中ctypes方法最简单,将C代码直接编译成dll即可使用,cython可以为现有的python代码加速,swig可以直接将C转换为python,就是有点麻烦,但速度是最快的。numba的jit加速比上面三种方法都要更简单,只需在python源码里加几句就行了,而且速度也很可观。

最后修改:2020 年 06 月 19 日
你的赞赏是我前进的动力