wedge前面我们说了Cython是什么,我们为什么要使用它,以及如何编译和运行Cython代码。有了这些知识,是时候深入Cython了。不过在此之前,我们还是需要深入分析一下Python和Cython的区别。Python和Cython的区别大体上不外乎两个,一个是:运行时解释和预编译;另一种是:动态类型和静态类型。解释执行和编译执行为了更好地理解为什么Cython可以提高Python代码的执行性能,有必要比较一下虚拟机执行Python代码和操作系统执行编译C代码的区别。Python代码在运行之前,会被编译成一个pyc文件(里面存放的是PyCodeObject对象),然后读取里面的PyCodeObject对象,创建栈帧,执行内部字节码。字节码是Python虚拟机可以解释或执行的基本指令集,而虚拟机是独立于平台的,因此在一个平台上生成的字节码可以在任何平台上运行。虚拟机将高级字节码翻译成一个或多个低级操作(指令),这些操作可以由操作系统调度,由CPU执行。这种虚拟化很普遍,也很灵活,可以带来很多好处:其中一个好处就是不会被挑剔的操作系统拒绝(相对于编译型语言,你在一个平台上编译的可执行文件可能不会被拒绝其他平台不会使用),缺点是运行速度比本地编译的代码慢。从C的角度来说,既然没有虚拟机,也就没有所谓的高级字节码。C代码直接编译成机器码,以可执行文件或动态库(.dll或.so)的形式存在。但注意:它依赖于当前的操作系统,是为当前的平台和架构量身定做的,可以直接由CPU执行,而且级别很低(伴随着速度很快),所以和操作系统有关它位于。那么有没有办法弥补虚拟机的字节码和CPU的机器码之间的宏观差异呢?答案是肯定的,即C代码可以编译成一种特定类型的动态库,称为扩展模块,这些库可以作为成熟的Python模块使用,只是里面的内容已经被编译成机器码了。Python虚拟机在导入扩展模块执行时,不再解释高层字节码,而是直接运行机器码,可以去除性能开销。这里再提一下扩展模块。我们说Windows有一个.dll(动态链接库),Linux有一个.so(共享文件)。如果只是用C或C++,甚至Go等编写的普通源文件,然后编译成.dll或.so,那么两者可以通过ctypes调用,但不能通过import导入。如果强行导入,会报错:ImportError:dynamicmoduledoesnotdefinemoduleexportfunction但是如果按照Python/CAPI编写,虽然编译后的扩展模块在Linux上也是.so和.pyd(Windows上的.pyd)也是一个.dll),但它们可以被解释器直接识别和导入。如果将一段普通的Python代码编译成扩展模块(Cython是Python的超集,即使是纯Python也可以编译成扩展模块),效率能提高多少?根据Python代码的作用,差异可能非常大,但通常将Python代码转换为等效扩展模块可提高10%到30%的效率。因为一般来说,代码既受IO限制又受CPU限制。所以即使没有任何Cython代码,纯Python编译成扩展模块后也会有性能提升。如果代码是计算密集型的,它会更有效率。Cython给了我们自由加速的便利,让我们不用写Cython,也就是只写纯Python就可以得到优化。但是这种只针对纯Python的优化,显然只是扩展模块的冰山一角。真正的性能提升是用Cython的静态类型代替Python的动态分析。因为Python没有进行基于类型的优化,即使编译成扩展模块,如果类型不确定,还是没有办法做到高效率。以两个变量相加为例:由于Python没有进行基于类型的优化,所以这行代码对应的机器码数量显然会很大。即使编译成扩展模块,对应的机器码个数也差不多(内部会有优化,机器码个数可能会少一些,但不会少多少)。两者的区别在于:普通模块有翻译过程,将字节码翻译成机器码;而扩展模块都已经提前翻译成机器码。但是CPU在执行的时候,由于机器码的个数差不多,所以执行时间也差不多。不同的是少了一个翻译过程。但显然,Python将字节码翻译成机器码所花费的时间几乎不用考虑,重点是CPU执行机器码所花费的时间。因此,将纯Python代码编译成扩展模块并不会增加太多速度。10-30%的提升也是Cython编译器的内部优化。例如,如果在函数结束后发现函数中的某个对象未被使用,则将其分配到堆栈上等等。如果在使用Cython时指定了类型,由于类型确定,机器代码量大大减少。不言而喻,CPU执行10条机器码的时间比执行1条机器码的时间要长。所以,在使用Cython的时候,关键的一点就是指定类型。一旦确定了类型,速度就会快很多。动态类型与静态类型Python语言与C、C++的另一个重要区别是前者是动态语言,后者是静态语言。静态语言要求必须在编译时确定变量的类型,这通常通过显式声明来完成。另一方面,一旦声明了一个变量,此范围内的变量类型以后就不能更改。看起来限制还是挺多的,那么静态类型能带来什么好处呢?除了编译时类型检测,编译器还可以基于静态类型生成适应当前平台的高性能机器码。动态语言(针对Python)则不同。对于动态语言,类型不绑定到变量,而是绑定到对象。变量只是指向对象的指针。因此,在Python中如果要创建一个变量,必须在创建的同时赋值,否则解释器不知道该变量指向哪个对象。在像C这样的静态语言中,可以创建一个变量而不赋初值,比如:intn,因为你已经知道n是int类型,所以分配空间的大小就已经确定了。而对于动态语言,变量可以指向任何对象,即使它们在同一范围内,因为变量只是一个指针。举个栗子:var=666var=《python编程学习圈》首先是var=666,相当于创建一个整数666,然后让变量var指向它;然后是一个var="python编程学习圈",然后会创建一个字符串,然后让var指向这个字符串。或者var不存储整数666的地址,而是存储新创建的字符串的地址。所以在运行Python程序时,解释器会花费大量时间来确认执行的低级操作并提取相应的数据。考虑到Python设计的灵活性,解释器总是需要以非常通用的方式来确定相应的低级操作,因为Python变量可以随时指向任何类型的数据。以上就是所谓的动态分析,Python一般的动态分析比较慢,还是a+b:*1)解释器需要检测a指向的对象的类型,这至少需要在a处进行一次指针查找C级;2)解释器从类型中寻找addition方法的实现,这可能需要一次或多次额外的指针查找和内部函数调用;3)如果解释器找到了对应的方法,那么解释器就有了一个实际的函数调用;4)解释器会调用这个加法函数,并将a和b作为参数传递;5)Python对象在C中都是一个结构体,比如:整数在C中是PyLongObject,还有Referencecount,type,ob_size,ob_digit,不用管这些成员是什么。简而言之,其中一个成员必须存储特定值,而其他成员存储附加属性。而加法函数显然必须从这两个结构中提取实际数据,这需要指针查找并将数据从Python类型转换为C类型。如果成功,则执行实际的加法操作;如果不成功,比如类型不对,发现a是整数,b是字符串,就会报错;6)加法运算完成后,必须将结果转换回Python对象,然后获取它的指针,转换成PyObject,然后返回;以上是Python中执行a+b的过程,而C语言在a+b的情况下表现不同。因为C是静态编译语言,C编译器在编译时决定了要执行的低级操作和要传递的参数数据。在运行时,已编译的C程序几乎跳过了Python解释器必须执行的所有步骤。对于a+b,编译器预先确定了类型,比如整数,所以编译器生成的机器码指令很少:将数据加载到寄存器中进行加法运算,然后存储结果。所以我们看到编译后的C程序几乎所有的时间都花在调用快速C函数和执行基本操作上,而没有Python的花里胡哨。由于静态语言对变量类型的限制,编译器会生成更快、更专业的指令,以适应它们的数据和它们所在的平台。所以,C语言比Python快几十倍甚至上百倍,再正常不过了。Cython之所以能带来如此巨大的性能提升,是因为它将C的静态类型引入到Python中,静态类型将运行时的动态分析转化为基于类型优化的机器码。在Cython之前,我们只能通过用C重新实现Python代码,即在C中编写所谓的扩展模块来受益于静态类型。但是Cython的出现简化了这一点,让我们可以在使用C的静态的同时编写类似于Python的代码类型系统。以上就是本次分享的全部内容。想了解更多python知识,请前往公众号:Python编程学习圈,发“J”免费领取,每日干货分享
