第一节 cython的潜能
•Cython是一种编程语言,它将Python与C和C ++的静态类型系统相结合。
•Cython是一个将Cython源代码转换为高效的C或C ++源代码的编译器。然后可以将此源代码编译为Python扩展模块或独立可执行文件。
Cython的强大功能来自它结合了Python和C的方式:感觉就像Python一样,同时提供了对C语言的轻松访问.Cython位于高级Python和低级C之间;有人可能会称它为克里奥尔语编程语言。但是Python和类C语言是如此不同 - 为什么要将它们结合起来?正是因为他们的差异是互补的。 Python具有高级,动态,易学和灵活的特点。然而,这些积极因素带来了成本 - 因为Python是动态的并且是解释的,它可能比静态类型编译慢几个数量级。另一方面,C是广泛使用的最古老的静态类型编译语言之一,因此编译器已经有近半个世纪的时间来优化其性能。 C非常低级且非常强大。与Python不同,它没有很多安全措施,并且很难使用。
两种语言都是主流,但鉴于它们的不同,它们通常用于不同的领域。 Cython的美妙之处在于:它结合了Python的表现力和C的强劲的性能,同时写起来仍然感觉像Python。除了极少数例外,Python代码(版本2.x和3.x)已经是有效的Cython代码。 Cython在Python语言中添加了少量关键字,以利用C的类型,允许cython编译器生成有效的C代码。如果您已经了解Python并且对C或C ++有基本的了解,那么您将能够快速学习Cython。您不必学习另一种新的语言。我们可以将Cython视为一个介于python和c或c++中间的项目。如果将Python编译为C语言是Cython的反面,那么将C或C ++与Python接口就是它的正面。我们可以从需要更好性能的Python代码开始,或者我们可以从需要优化Python接口的C(或C ++)代码开始。为了加速Python代码,Cython使用可选的静态类型声明编译Python源代码,以根据算法实现大规模的性能改进。要使用Python连接C或C ++库,我们可以使用Cython与外部代码进行交互并创建优化的包装器。这两种功能 - 编译Python和与外部代码接口 - 都可以很好地协同工作,每个功能都是Cython有用的重要组成部分。
(补充:
Cython和CPython的区别
Cython经常与CPython混淆(请注意P),但两者是非常不同的。
CPython是标准和最广泛使用的Python实现的名称。
CPython的核心是用C语言编写的,CPython中的C用于区别于Python语言规范和其他语言的Python实现,例如Jython(Java),IronPython(.NET)和PyPy(Python中实现的))。 CPython为Python语言提供了一个C级接口;该接口称为Python / C API。 Cython广泛使用这个C接口,因此Cython依赖于CPython。 Cython不是Python的另一个实现 - 它需要CPython运行时来运行它生成的扩展模块。
)
我们来看一个例子吧。
第二节 比较Python,C和Cython
考虑一个简单的Python函数fib,它计算第n个Fibonacci数:1
def fib(n):
a,b=0.0,1.0
for i in range(n):
a,b=a+b,a
return a
正如在介绍中提到的,这个Python函数已经是一个有效的Cython函数,在Python和Cython中它具有相同的行为。 我们很快就会看到我们可以为fib添加基于Cython的特定语法来提高其性能。
下面是纯C语言的实现:
double cfib(int n){
int i;
double a=0.0,b=1.0,tmp;
for(i=0;i<n;i++){
tmp=a;a=a+b;b=tmp;
}
return a;
}
我们在C版本中使用double并在Python版本中以float进行比较直接并删除与C数据类型的整数溢出相关的任何问题。想象将C版本中的类型与Python版本中的代码混合。结果是一个静态类型的Cython版本:
def fib(int n):
cdef int i
cdef double a=0.0,b=1.0
for i in range(n):
a,b=a+b,a
return a
如前所述,Cython和Python代码非常类似,因此我们未经修改的Python fib函数也是有效的Cython代码。转换动态类型的Python版本 为静态类型的Cython版本,我们使用cdef 的Cython语句来声明静态类型的C变量i,a和b。即使对于之前没有看过Cython代码的读者,也是非常直观好理解的。具体的性能比较见下表:
![04e54c8c658e0b846b257f6c35048030.png](https://i-blog.csdnimg.cn/blog_migrate/15529ff11dae156fa27736e1207346fe.jpeg)
在表1-1中,第二列的测量fib(0)和第三列的运行时间
测量是其它三种方式相对于Python的fib(0)的加速。因为fib的参数控制循环迭代的次数,所以fib(0)不会进入Fibonacci循环,因此它的运行时间是语言运行时和函数调用开销的合理度量。(emmm。。。实际上在工业级项目上这一点非计算型的开销 时间还是可以接受的)
第四和第五列测量fib(90)的运行时和加速,其执行循环90次。调用开销和循环执行运行时都有助于其运行时。第六和第七列测量fib(90)运行时和fib(0)运行时之间的差异以及相对加速。这种差异仅仅是循环运行时的近似值,消除了运行时和调用开销。可以看到纯C,C extension以及cython的表现非常类似。
Table 1-1有四行:
纯Python
第一行(在标题之后)测量纯Python版本的fib的性能,并且如预期的那样,在所有类别中,它具有显着的最差性能。特别是,fib(0)的调用开销在该系统上超过半微秒。 fib(90)中的每个循环迭代需要将近150纳秒; Python留下了很大的改进空间。
纯C.
第二行测量纯C版本的fib的性能。在这个版本中,没有与Python运行时的交互,因此最小化 了调用开销;这也意味着它无法从Python中使用。这个版本提供了一个我们可以合理地期望从简单的序列fib函数中获得最佳性能。与Python相比,fib(0)值表示C函数调用开销最小(2纳秒),并且fib(90)运行时(164纳秒)比此特定系统上的Python快近80倍。
手写C扩展
第三行测量Python 2的手写C扩展模块
扩展模块需要几十行C代码,其中大部分都是调用Python / C API的样板。从Python调用时,扩展模块必须将Python对象转换为C数据,计算C中的Fibonacci数,然后进行转换结果返回Python对象。它的调用开销(fib(0)列)相应地大于纯C版本,后者不必从Python对象转换。因为它是用C语言编写的,所以对于fib(0),它比纯Python快三倍。它还为fib(90)提供了一个很好的30倍加速。(https://blog.csdn.net/fitzzhang/article/details/79212411 找到一篇关于c-extension的介绍, 对于python中的C extension并没有太多研究,大概的理解就是用C来写后端的逻辑然后转化为dll这类文件再用python导入)
大概长这样,之后还要进行编译和扩展,具体的可以参考上面的链接里的介绍
#include <Python.h>
static PyObject *module_func(PyObject *self, PyObject *args) {
/* Do your stuff here. */
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
{ "func", (PyCFunction)module_func, METH_VARARGS, NULL },
{ NULL, NULL, 0, NULL }
};
PyMODINIT_FUNC initModule() {
Py_InitModule3(Module, module_methods, "docstring...");
}
---------------------
作者:fitzzhang
来源:CSDN
原文:https://blog.csdn.net/fitzzhang/article/details/79212411
版权声明:本文为博主原创文章,转载请附上博文链接!
这里有一个问题就是,使用这种c-extension的方式会造成数据转化的开销,就是调用c-extension写好的逻辑的实现时,需要将python的数据类型转化为c的数据类型然后在c中进行运算,最后把计算的结果再由c的数据类型转化为python的数据类型。
我在使用julia写后端逻辑然后通过pyjulia导入的用julia写的逻辑函数的时候,没有直接用纯julia实现相同的功能来得快,就是在这个数据转化上的开销。这一块的话,打算还是放到julia的那个专栏里写好了,展开说太多了麻烦死了。
但是没有办法,目前公司业务上使用到的代码中python占了很大一部分,如果要完全替换成julia或者纯C势必会非常非常麻烦,而且虽然python速度相对慢,但是在大部分的场景下,通过调用多进程的方式可以把时间降低到可接受范围内,只不过可能有一些核心的功能,有一些特殊的时间上的要求,这个时候通过cython、c或c++、julia等等这类高性能的解决方案来解决会显得比较方便而优雅,而且还能多掌握一门使用(装逼)技术。总的来说,可以把python比喻成没有拳套的灭霸,cython、c++等等这些算是无限宝石了。
Cython
最后一行测量Cython版本的性能。与C扩展一样,它可以从Python中使用,因此它必须先将Python对象转换为C数据,然后才能计算Fibonacci数,然后将结果转换回Python。由于这种开销,它无法与fib(0)的纯C版本匹配,但值得注意的是,它的开销比手写C扩展的开销快2.5倍。由于这种降低了的调用开销,它能够为纯粹的Python(90)提供大约50倍的加速。
table1-1中的内容是最后两列:纯粹的循环运行时C,C扩展和Cython版本在这个系统上都是165纳秒,并且相对于Python的加速比都大约是75倍。
Cython通常可以生成与纯C等效的高效代码。因此,在正确计算Python开销时,我们看到Cython达到了C级性能。 而且,它比手写的C扩展模块更好,在Python-to-C转换上比C扩展模块的开销更小这是核心原因。
Cython生成高度优化的代码,通常比一个等效的手写C扩展模块更快 。它经常能够 生成Python-to-C转换代码,这是比使用Python / C API的原始调用快的几个因素之一。
正如我们将在第3章中学到的,我们可以更进一步,使用Cython来创建没有Python开销的Python类C函数。但Cython代码不能直接从Python调用。它们允许我们为核心计算移除昂贵的调用开销。
Cython的性能改进是什么原因?对于此示例,可能的原因是函数调用开销,循环,数学运算以及堆栈与堆分配。
(1)函数调用的开销
fib(0)运行的时间主要是由相应语言调用函数所花费的时间来决定的,我们在表1-1中看到,Cython生成的代码比调用Python函数快了近一个数量级,比手写C扩展快两倍多。 Cython通过传递一些较慢的Python / C API调用来生成高度优化的C代码来实现这一点。我们在前面的C扩展时序中使用这些API调用。
(2)循环
与编译语言相比,Python for循环非常慢。加速循环Python代码的一种可靠方法是找到将Python for和while循环移动到编译代码中,方法是调用内置函数或使用Cython之类的东西为您进行转换。通过表中的fib(90)列以每种语言运行for循环90次迭代的结果我们可以很明显的感受到循环对于时间效率上的影响。(numba实际上也可以针对python的循环进行优化不过没有系统进行比较过,numba的tutorial在github上面也有,有空也写一写)
(3)数学运算
因为Python是动态类型的,不能进行任何基于类型的优化,所以像 a+ b 这样的表达式可以做任何事情。我们可能知道a和b只会成为浮点数,但Python从未做过这种假设。所以,在运行时,Python必须查找a和b的类型(在本例中,它们是相同的)。然后它必须找到类型的底层__add__方法(或等效方法),并使用a和b作为参数调用__add__。在这个过程中,必须先将Python的浮点数a和b的转化为底层C的double数据类型,只有这样,才能进行实际的添加!此添加的结果必须打包在 全新的Python浮点并作为结果返回。
(cpython,注意是cpython不是cython,这里补充一下基础知识:
CPython:是用C语言实现Pyhon,是目前应用最广泛的解释器。最新的语言特性都是在这个上面先实现,基本包含了所有第三方库支持,但是CPython有几个缺陷,一是全局锁使Python在多线程效能上表现不佳,二是CPython无法支持JIT(即时编译),导致其执行速度不及Java和Javascipt等语言。于是出现了Pypy。。
Python的解释器:
由于Python是动态编译的语言,和C/C++、Java或者Kotlin等静态语言不同,它是在运行时一句一句代码地边编译边执行的,而Java是提前将高级语言编译成了JVM字节码,运行时直接通过JVM和机器打交道,所以进行密集计算时运行速度远高于动态编译语言。
)
C和Cython版本已经知道a和b是double数据类型的,所以对a和b进行add操作只需要编译一个机器代码指令。
(4)堆栈与堆分配
在C的层面上,动态Python对象完全是堆分配的。 Python非常努力地使用内存池智能地管理内存,并内化常用的整数和字符串。但事实仍然是创造和摧毁对象- 任何对象,甚至是标量 - 都会产生动态分配内存和Python内存子系统的开销。由于Python浮点对象是不可变的,因此使用Python浮点数的操作涉及堆分配对象的创建和销毁。 Fib的Cython版本将所有变量声明为堆栈分配的C的双精度数。
通常,堆栈分配比堆分配快得多。此外,C浮点数是可变的,这意味着for循环体在分配和内存使用方面更有效。因为Python循环体必须在每次迭代中完成更多的工作,所以C和Cython版本比纯Python快一个数量级并不奇怪。(这一点其实我不是很理解,有理解的大佬求评论调教)
泼一盆冷水
当我们向Python代码添加一些简单的cdef语句时,看到大量的性能改进是令人振奋的。但是,值得注意的是,在使用Cython编译时,并非所有Python代码都会看到大量的性能改进。前面的fib示例是故意CPU绑定的,这意味着所有运行时间都花在操作CPU寄存器内的一些变量上,几乎不需要数据移动。相反,如果此函数是内存绑定(例如,添加两个大型数组的元素),I / O绑定(例如,从磁盘读取大文件)或网络绑定(例如,从FTP服务器下载文件),Python,C和Cython之间的性能差异可能会显着降低(对于内存绑定操作)或完全消失(对于I / O绑定或网络绑定操作)
当改进Python的性能是目标符合帕累托原则(帕累托法则,又叫二八法则、80/20原理、帕累托效应。它是指,在任何特定 群体中,重要的因子通常只占少数,而不重要的因子则占多数,因此只要能控制具 有重要性的少数因子即能控制全局。即80%的价值是来自20%的因子,其余的20%的价值则来自80%的因子。)时:我们可以预期程序运行时间中的大约80%仅由代码的20%引起。那么我们首先要分析一大段python代码中,占用了大部分运行时间的代码块在哪里,然后分析它产生太多时间开销的原因,如果我们通过分析确定我们程序中的瓶颈是由于它是I / O开销高或网络速度的限制,那么我们不可能指望Cython在性能上提供显着的改进。在转向Cython之前确定你遇到的性能瓶颈是可以通过cython来解决的,cython它是一个强大的工具,但它必须用正确的方法来使用。因为Cython将C类型系统引入Python,所以C数据类型的所有限制都成为相关问题。在计算大值时,Python整数对象以静默方式转换为无限精度的Python长对象。 C int或long是固定精度的,意味着它们无法正确表示无限精度整数。 Cython具有帮助捕获这些溢出的功能,但更重要的一点仍然是:C数据类型比它们的Python中对应的数据类型要快,但有时不是那么灵活就是了。
让我们考虑一下Cython的另一个重要功能:与外部代码连接。假设我们的从是C或C ++代码的角度来考虑,而不是Python代码,我们想为它创建Python包装器。因为Cython了解C和C ++声明并且可以与外部库接口,并且因为它可以生成高度优化的代码,所以我们很容易用它来为c或c++编写高效的包装器。
用Cython包装C代码
继续我们的Fibonacci主题,让我们从C实现开始,并使用Cython将其包装在Python中。我们函数的接口在cfib.h中:
double cfib(int n){
int i;
double a=0.0,b=1.0,tmp;
for(i=0;i<n;i++){
tmp=a;a=a+b;b=tmp;
}
return a;
}
cfib.h的Cython包装器代码少于10行:
cdef extern from "cfib.h":
double cfib(int n)
def fib(n):
"""Returns the nth Fibonacci number."""
return cfib(n)
我们在cdef extern语句中提供cfib.h头文件名,并在块的缩进体中声明cfib函数的签名。在cdef extern块之后,我们定义了一个fib的 Python包装函数,它调用cfib并返回其结果。
在将前面的Cython代码编译成名为wrap_fib的扩展模块之后(我们将在第2章中介绍如何编译Cython代码的细节),我们可以在python中这么使用它:
![b01665932a84d4710240f62c3ff405e5.png](https://i-blog.csdnimg.cn/blog_migrate/8f5294056de1207c56aad2488ab3e4f4.jpeg)
我们看到fib函数是wrap_fib扩展模块中的常规Python函数,并使用Python中的整数90作为参数调用fib函数,为我们调用底层C函数并返回我们期望的结果。总的来说,只使用很少量的Cython代码就可以包装一个简单的函数。而如果要进行手写C扩展的话,一个手写的包装器会需要几十行C代码,以及熟悉Python / C API的详细知识,性能还没cython好。
这个例子很简单。如果值在范围内,则Python int会毫无问题地转换为C int,否则会引发OverflowError。在内部,Python float类型将其值存储在C double中,因此cfib返回类型没有转换问题。因为我们使用简单的标量数据,所以Cython可以自动生成类型转换代码。在以后的章节中,我们将看到Cython如何帮助我们包装任意复杂的数据结构,类,函数和方法。因为Cython是一种成熟的语言(而不仅仅是像其他包装工具那样的面向对象的特定于域的语言),我们可以使用它在包装函数调用之前和之后做任何我们喜欢的事情。因为Cython语言理解Python并且可以访问Python的标准库,所以我们可以充分利用Python的所有功能和灵活性。
应该注意的是,我们可以在一个文件中使用Cython的两个raisons d'être (what the fuck??不知道原文写的这个什么意思)- 加速Python以及调用外部C函数(牛逼,鼓掌)。我们甚至可以在同一个函数内完成!我们将在以后的章节中看到这一点。
补充:Cython的起源
Greg Ewing是Cython的前身Pyrex的作者。当Pyrex首次发布时,它通过大量方法加速Python代码的能力使它立即流行起来。许多项目采用它并开始密集使用它.Pyrex并不打算支持Python语言中的所有构造,但这并没有限制其最初的成功 - 它满足了迫切的需求,特别是对于科学计算领域。正如成功的开源项目经常出现的那样,其他项目组改编并修补Pyrex以满足他们的需求。在Robert Bradshaw和Stefan Behnel的领导和指导下,Stefan Behnel和William Stein的两个Pyrex-one叉子最终结合起来形成了Cython项目。
自Cython成立以来,William Stein的Sage项目一直是其发展的主要推动力。 Sage是一个GPL许可的综合数学软件系统,旨在为Magma,Maple,Mathematica和Matlab提供可行的替代方案。
Sage广泛使用Cython来加速以Python为中心的算法,并与数十个C,C ++和Fortran库进行交互。这是现存最大的Cython项目,拥有数十万行Cython代码。如果没有Sage的支持,Cython很可能不会得到持续的初始支持,成为现在的样子:一个独立的,广泛使用的,积极开发的开源项目。自创建以来,Cython已经拥有了广阔的目标,首先是完全兼容Python。它还获得了特定于Python和C之间独特位置的功能,使Cython更易于使用,更高效,更具表现力。这些Cython的特色功能包括:
C类型和Python类型之间更容易互操作和转换;
专门的语法,以简化包装python和C ++的接口;
特定代码路径的自动静态类型推断;
具有特定缓冲区语法的一流缓冲区支持(第10章有介绍);
类型化内存视图(第10章);
基于线程的Prange并行(第12章,这个功能牛逼!)
该项目在其一生中获得了NSF(通过Sage),华盛顿大学,Enthought(作者的雇主)和几个Google Summer of Code项目(其中一个项目资助了作者2009年的Cython开发)的资金和支持。 除了明确的资金支持外,Cython还从一个庞大而活跃的开源社区中受益,他们花费了大量时间和精力来开发新功能,实现它们,报告错误并修复它们。
总结
本章旨在激发读者的兴趣。 我们已经看到了Cython的基本功能,它们被提炼为最基本的元素。 本书的其余部分深入介绍了Cython语言,介绍了如何编译和运行Cython代码,介绍了如何与其进行C和C ++的交互,并提供了许多示例来帮助您在自己的项目中有效地使用Cython。
本篇从整体上介绍了一下cython,啊,迫不及待要用到实战中去了,回头再开一个cython实战的专栏,专门用来放实际上项目中代码的改进和时间开销的测试算了。。