震惊!加速X个数量级!Cython到底有多快
三次元魔法师
2021年07月23日 13:48
收录于文集
共1篇

目前来说,处理数据我还是比较喜欢使用pandas,确实爽到飞起,而这种易用性带来的是底层数据结构的复杂性,从而导致性能损失。但是好用真的太重要了,所以出现了像Numba/Datatable等一些列支持并行化的Dataframe的方案,当然也有dask这一类分布式并行架构,话说回来 Numba已经支持GPU加速~说起来还是挺爽的,但是这不是文章的重点,关于pandas并行,前面已经有一篇文章介绍过了。

这次来我们聊一下加速python的另一种思路,C语言加速,这里我们使用Cython,sklearn大多数计算由这个方案实现。当然可以利用C语言直接扩展python,PyTorch/TensorFlow/Numpy都是这种方案,但是这个需要对C开发比较熟悉,开发效率可能是不及Cython的,可能哈。

为什么python慢,因为它是动态数据类型,运行时解释器要花费大量时间来确定对象的数据类型,从而判定数据类型的属性,C语言等一些列严格的静态数据类型语言就没有这些遗憾,所以C的效率要高很多,在一些特殊情况下能高出几个数量级。Cython的原始文档:

This can make Python a very relaxed and comfortable language for rapid development, but with a price - the ‘red tape’ of managing data types is dumped onto the interpreter. At run time, the interpreter does a lot of work searching namespaces, fetching attributes and parsing argument and keyword tuples. This run-time ‘late binding’ is a major cause of Python’s relative slowness compared to ‘early binding’ languages such as C++.

This指的是python的动态数据类型优势。

直接看例子,来自这篇文章:https://pythonprogramming.net/introduction-and-basics-cython-tutorial

Python版的函数:

代码块
Python
自动换行
复制代码
# example_original.py
def test(x):
    y = 0
    for i in range(x):
        y += i
    return y
复制成功

CPython版的函数:

代码块
Python
自动换行
复制代码
# example_cython.pyx
cpdef int test(int x):
    cdef int y = 0
    cdef int i
    for i in range(x):
        y += i
    return y
复制成功

Cython文件后缀名是".pyx&#​34;,相比原生python方法,增加了数据类型的定义,接下来需要编写setup.py文件用于构建pyx文件

代码块
Python
自动换行
复制代码
from distutils.core import setup
from Cython.Build import cythonize

setup(ext_modules = cythonize('example_cython.pyx'))
复制成功

三个文件都是在同一个目录下,进shell执行:

代码块
Python
自动换行
复制代码
python setup.py build_ext --inplace
复制成功

一切顺利的话~~会得到一个Warning~~

FutureWarning: Cython directive 'language_level&#​39; not set, using 2 for now (Py2). This will change in a later release!

可以在pyx文件中加入如下声明:

代码块
Python
自动换行
复制代码
# cython: language_level=3
复制成功

没什么问题就OK了,接下来写一个test文件:

代码块
Python
自动换行
复制代码
import example_cython, example_original, time

if __name__ == '__main__':
    times = 10000
    add_times = 100

    # original
    original_total_elapse = 0.0
    for i in range(times):
        start_time = time.time()
        example_original.test(add_times)
        original_total_elapse += time.time() - start_time

    # cython
    cython_total_elapse = 0.0
    for i in range(times):
        start_time = time.time()
        example_cython.test(add_times)
        cython_total_elapse += time.time() - start_time

    print("Cython is {}x faster.".format(original_total_elapse / cython_total_elapse) )
复制成功

测试代码比较简单,调用函数times次,累加每次执行这个函数的时间,我这边的输出:

代码块
Python
自动换行
复制代码
$ python testing_things.py
Cython is 23.1180884220247x faster
复制成功

没错哈,加速了20多倍,这个时候你就会骂我标题党了~~并没有哈~~如果我们把上面times和add_times换一下,也就是循环100次,每次累加10000次,输出结果就会出现比较大的差异:

代码块
Python
自动换行
复制代码
$ python testing_things.py
Cython is 1056.4248704663212x faster
复制成功

没骗人吧,确实快了一千倍,这样看来还是很恐怖的。

之所以第一次实验加速只有20倍,原因大概是因为我们循环了10000次,也就是python调用了10000次C写的函数,而python在调用C函数的交互会消耗大量资源。

根据这个特性,Cython加速的应用场景应该是调用次数比较少,函数内部计算循环比较多的情况。

除此之外,Numpy/PyTorch/Tensorflow一类的框架,内部数值计算的时候已经做了相应的加速,比如矩阵乘法,所以如果要做数值计算类的千万不要自己手动写,能调numpy就调numpy。