赞
踩
因为《Cython》第三章的内容太多了,所以这里分成两部分,这里是第一部分。
前段时间把cython-tutorial的教程撸了一遍,感觉大体来看,cython入门不难,提高个十几倍的速度也比较easy,但是如果要达到上百倍的速度就需要对c语言或c++比较了解了,cython的c部分的语法基本和c或者c++是一样的,所以实际上用cython写地道的c或者c++不失为一举两得的好方法:一方面能够学会如何将代码性能提高,一方面也熟悉了c或c++的编程方法。
接下来的学习路线分两条,一条是继续把《cython》这本书翻译完成,一条是把《机器学习实战》的算法结合进来,《机器学习实战》作为一本入门级的算法书,所有的算法都是用纯python实现的,用来优化确实很方便,有空做一个性能的比较,把纯python和python+cython+numba+pyjulia混编结合起来进行比较,当然也可以分别进行优化,一方面熟悉算法的开源实现一方面练手。
废话不多说,进入正文。
前面的章节介绍了Cython是什么,我们为什么要使用它,以及我们如何编译和运行Cython代码。 掌握了这些知识,是时候深入探索Cython语言了。本章的前两部分介绍了Cython工作的深层原因以及加速Python代码的本质原因。 这些部分对于帮助形成Cython如何工作的思维框架非常有用,但是不需要理解Cython的语法,语法包含在其它剩余的章节。对于那些对Cython工作原理感兴趣的人,可以将cython加速python的原因归结为两个不同之处:运行时间解释与提前编译,以及动态与静态类型。
解释与编译执行
为了更好地理解Cython如何以及为何能够提高Python代码的性能,比较Python运行时如何运行Python代码的方式与操作系统运行编译后的C代码的方式对于理解将会非常有用。
在运行之前,Python代码会自动编译为Python字节码。字节码是由Python虚拟机(VM)执行或解释的基本指令。因为VM抽象出所有特定于平台的细节,所以Python字节码可以在一个平台上生成并在其他任何地方运行。 VM可以将每个高级字节码转换为一个或多个低级操作,这些操作可以由操作系统以及最终的CPU执行。这种虚拟化设计很常见且非常灵活,并且带来了许多好处 ——它们不需要考虑挑剔的本地编译器!而主要缺点是VM比运行本机编译的代码慢。
而对于c语言来说,没有VM或解释器,并且没有高级字节码。 C代码由编译器直接转换或编译为机器代码。此机器代码包含在可执行文件或编译库中。它是针对特定的平台和体系结构量身定制的,它可以直接由CPU运行,并且它的级别低得多。有一种方法可以弥补字节码执行VM和机器代码执行CPU之间的鸿沟:那就是使Python解释器可以直接,透明地向最终用户运行已编译的C代码。必须将C代码编译为特定类型的动态库,称为扩展模块。这些模块是完整的Python模块,但其中的代码已由标准C编译器预编译为机器代码。在扩展模块中运行代码时,Python VM不再解释高级字节码,而是直接运行机器代码。这将消除解释器的性能开销,同时此扩展模块中的任何操作都在运行。
那么Cython是如何工作的呢?正如我们在第2章中看到的,我们可以使用cython和标准C编译器将Cython源代码转换为特定于编译平台的扩展模块。每当Python在扩展模块中运行任何东西时,它就会运行编译代码,因此解释器开销不会减慢速度。解释与直接执行有多大区别?它可能会有很大差异,具体取决于所讨论的Python代码,但通常我们可以将Python代码转换为等效的扩展模块,速度将提高10%到30%不等。Cython免费为我们提供了这个加速(只需要%%cython或者通过setup来编译成windows下的pyd文件就可以)。但真正的性能改进来自于用静态类型替换Python的动态类型。
动态与静态的数据类型
Python,Ruby,Tcl和JavaScript等高级语言与C,C ++和Java等低级语言之间的另一个重要区别是前者是动态类型的,而后者是静态类型的。静态类型语言要求在编译时修复变量的类型。通常我们可以通过显式声明变量的类型来实现这一点,或者,如果可能,编译器可以自动推断变量的类型。在任何一种情况下,在使用它的上下文中,变量具有该类型且仅具有该类型。
静态数据类型带来了哪些好处?除了编译时类型检查,编译器还使用静态类型生成针对特定类型定制的快速机器代码。
动态类型语言对变量的类型没有限制:例如,相同的变量可以以整数开头,最后以字符串,列表或自定义Python对象的实例结束。动态类型语言通常更容易编写,因为用户不必显式声明变量的类型,而是在运行时捕获与类型相关的错误。在运行Python程序时,解释器花费大部分时间来计算执行各种低级操作,并提取数据以提供此低级操作。鉴于Python的设计和灵活性,Python解释器总是必须以完全通用的方式确定低级操作,因此变量可以随时具有任何类型。这被称为动态调度。(举个例子,我们在使用符号进行a+b的时候,如果a+b中a,b均为整数则此时“+”代表了代数加,如果是a、b是两个数组则表示数组连接,如果a、b是字符串则表示字符串连接,也就是python底层写了非常多复杂的判断,要判断数据类型还要根据不同的数据类型重定义“+”的逻辑功能,这样肯定慢啊)
由于许多原因,完全通用的动态调度很慢.1例如,考虑一下Python运行时评估a + b时会发生什么:
1.解释器检查a引用的Python对象的类型,这需要在C级别至少有一个指针查找。
2.解释器询问类型以实现加法方法,这可能需要一个或多个额外的指针查找和内部函数调用。
3.如果找到了相关方法,那么解释器就有一个它可以调用的实际函数,可以用Python或C语言实现。
解释器调用add函数并传入a和b作为参数。
5.加法函数从a和b中提取必要的内部数据,这可能需要多次指针查找和从Python类型到C类型的转换。
如果成功,那么它才能执行将a和b一起添加的实际操作。
6.然后必须将结果放在一个(可能是新的)Python对象中并返回。只有这样才能完成操作。
相比较而言,C的情况非常不同。由于C是经过编译和静态类型化的,因此C编译器可以在编译时确定要执行的低级操作以及要作为参数传递的低级数据。在运行时,编译的C程序几乎会跳过Python解释器必须执行的所有步骤。对于像a + b这样的东西,a和b都是基本的数字类型,编译器会生成一些机器代码指令,将数据加载到寄存器中,添加它们并存储结果,编译的C程序所有时间来调用快速的C函数和执行基本操作。由于静态类型语言对其变量的限制,编译器会生成更快,更专业的指令,这些指令是根据其数据量身定制的。鉴于这种效率,对于某些操作来说,像C这样的语言比Python快几百甚至几千倍的速度并不奇怪?
Cython产生如此令人印象深刻的性能提升的主要原因是它为动态语言带来了静态类型。静态类型将运行时动态调度转换为类型优化的机器代码。在Cython(以及Cython的前身Pyrex)之前,我们只能通过在C中重新实现我们的Python代码从而从静态类型中受益.Cython可以很容易地保持我们的Python代码,并使用C的静态类型系统。我们将学习的第一个也是最重要的Cython特定关键词是cdef,它是我们通往C实现的大门。
带cdef的静态类型声明
Cython中的动态类型变量和python中动态类型变量基本一样:我们只是分配给一个变量来初始化它并像在Python中一样使用它:
在Cython中,无类型动态变量的行为与Python变量完全相同。 赋值b = a允许a和b访问在前面示例中在第一行上创建的相同列表对象。 通过a[3] = 42.0修改列表会修改b引用的相同列表,因此assert成立。 赋值a = 13使b指向原始列表对象,而a指的是Python整数对象。 这是对更改类型的重新分配,这是完全有效的Python代码。 要在Cython中静态输入变量,我们使用带有类型和变量名称的cdef关键字。 例如:
使用这些静态类型变量看起来就像Python(或C)代码:
(补充:动态变量和静态变量之间的重要区别在于,静态数据类型需要服从c语言的各种语法和规范)
在前面的示例中,i = j将j处的整数数据复制到为i保留的存储器位置。 这意味着i和j指的是独立的实体,并且可以单独使用。与C一样,我们可以同时声明几个相同类型的变量:
在函数内部,cdef语句是缩进的,声明的静态变量是该函数的本地变量。下面的这些示例代码都是cdef在函数集成中声明局部变量的有效用法:
当然更方便的方法如下:
这就是cython中cdef的两种形式。
我们可以声明C支持的任何类型的变量。 表3-1给出了使用cdef进行更常见C类型的示例。
Cython支持全部的C语言的声明方法,甚至是指向函数指针的指针 例如,要声明一个将函数指针作为唯一参数并返回另一个函数指针的函数,我们可以这么写:
目前尚不清楚如何在Cython中使用signal函数,但后面的章节我们将学习使用cython中的C函数指针。 Cython不限制我们可以使用的C的数据类型,这在我们包装外部C库时特别有用。
Cython中的自动类型推断
使用cdef进行静态类型输入并不是在Cython中静态输入变量的唯一方法。 Cython还对函数和方法中的无类型变量执行自动类型推断。 默认情况下,只有这样做才能不改变代码的语义时,Cython才会推断变量类型。
考虑一下下面这个简单的函数体:
在此示例中,Cython将 1 和 3 + 4j 以及变量i,c和r 都视为为常规Python对象。 尽管这些类型具有明显的相应C类型,但Cython保守地假设整数i可能无法表示为C 长整型,因此将其类型化为具有Python语义的Python对象。 自动推理能够推断出2.0,因而变量d,是C的两倍并相应地进行。 对于最终用户来说,就好像d是一个普通的Python对象,但是Cython将它视为性能的C double。
通过infer_types编译器指令(参见第28页的“编译器指令”),我们可以给Cython更多的余地来推断可能改变语义的情况下的类型 - 例如,当整数加法可能导致溢出时。
对于一个函数,我们可以使用infer_types的装饰器形式来启用cython的类型推断功能 :
通过这个神奇的,所以变量i被输入为C long; d与之前一样是双精度,c和r都是C级复变量(更多关于表3-2中的复杂变量和第41页的“复杂类型”)。 启用infer_types时,我们负责确保整数操作不会溢出,并且语义不会从非类型化版本更改。 infer_types指令可以在函数作用域或全局启用,这样可以很容易地测试它是否会改变代码库的结果,以及它是否会对性能产生影响。
因为infer_types是为more_inference启用的,所以变量i被输入为C long;d与之前一样是双精度,c和r都是C级复变量(更多关于表3-2中的复杂变量和第41页的“复杂类型”)。 启用infer_types时,我们负责确保整数操作不会溢出,并且语义不会从非类型化版本更改。 infer_types指令可以是在功能范围或全局启用,可以轻松测试它是否更改了代码库的结果,以及它是否会对性能产生影响。
cython中的C指针
正如我们在表3-1中看到的,在Cython中声明C指针使用C语法和语义:
float ** 表示二级指针,float* 是一级指针,其中存放着变量的地址,指针本身也是存在内存地址的,所以二级指针存放着一级指针的地址。
一行上可以声明多个指针,我们必须使用星号来声明每个变量,如下所示:
如果我们使用:
这声明了一个整数指针a和一个非指针整数b, 在最近的版本中,Cython在编译容易出错的声明时会发出警告。 在Cython中取消引用指针与在C中的指针不同。因为Python语言已经使用* args和** kwargs语法来允许任意位置和关键字参数和支持函数参数解包,Cython不支持* a语法来取消引用C指针(也就是说cython中不支持通过*来访问指针指向的变量值)。 相反,我们索引到位置0处的指针以取消引用Cython中的指针。 这种语法也适用于取消引用C中的指针,虽然这种情况很少见。例如,假设我们有一个golden_ratio C double和一个p_double C指针:
我们可以像c语法那样对二者进行指定:
cython针对通过指针修改变量的问题所提供的操作方式为:
同样我们能够以类似的方法来访问指针中对应的变量值:
或者,我们可以使用cython.operator.dereference 这个操作符来达到c中“*”引用的作用。 我们通过cimport从特殊的cython命名空间访问这个操作符,这将在第6章中详细介绍:
不过这种访问指针指向变量的方式用的比较少,还是通过访问0这种方式比较方便简单。
当我们使用指向结构的指针时,Cython和C之间出现了另一个区别。 (我们将在本章后面深入介绍Cython的结构支持。)在C中,如果p_st是指向struct typedef的指针:
如果要在p_st内部访问一个struct成员,我们使用箭头语法:
但是,无论我们是否有非指针结构变量或指向结构的指针,Cython都使用点访问:
无论我们在C中使用箭头运算符还是点运算符,我们都在Cython中使用点运算符,Cython将生成正确的C级代码。
混合静态和动态类型的变量
Cython允许在静态和动态类型变量之间进行分配。 这种静态和动态的流畅混合是一个强大的功能,我们将在几个实例中使用它:它允许我们为我们的大多数代码库使用动态Python对象,并轻松地将它们转换为快速的静态类型,用于提升性能。
为了说明,假设我们有几个(静态)C int,我们想要组成一个(动态)Python元组。 使用Python / C API创建和初始化这个元组的C代码很简单但很乏味,需要几十行代码,并且需要进行大量的错误检查。 在Cython中,显而易见的方法就是:
这段代码很简单,这里要强调的一点是a,b和c是静态类型的整数,Cython允许用它们创建动态类型的Python元组文字。 然后我们可以将该元组分配给动态类型的tuple_of_ints变量。 这个例子的简单性是Cython的高性能和优雅的一部分:我们可以用显而易见的方式创建一个C int元组,而无需进一步思考。 我们希望概念上这样简单的东西很简单,这就是Cython提供的东西。
这个例子有效,因为C int和Python int之间有明显的对应关系,因此Python可以为我们自动转换。 如果a,b和c是例如C指针,则此示例将不起作用。 在这种情况下,我们必须在将它们放入元组之前取消引用它们,或者使用其他策略。
表3-2给出了内置Python类型与C或C ++类型之间的对应关系的完整列表。
表3-2。 键入内置Python类型与C或C ++类型之间的对应关系
关于表3-2,有几点值得一提,我们将在下面讨论。
bint类型
bint布尔整数类型是C级别的int,并且是从Python bool转换而来的。 它具有标准C对真实性的解释:零为假,非零为真。
类型转换和溢出
在Python 2中,Python int存储为C long,而Python long存在无限精度。 在Python 3中,所有int对象都是无限精度。将整数类型从Python转换为C时,Cython会生成检查溢出的代码。 如果C类型不能表示Python整数,则会引发运行时OverflowError。有一些相关的布尔值溢出检查和overflowcheck.fold编译器指令(参见第28页的“编译器指令”),它们将在我们使用C整数时捕获溢出错误。 如果overflowcheck设置为True,Cython将引发OverflowError以溢出C整数算术运算。 设置overflowcheck.fold指令可能有助于在启用overflowcheck时消除一些开销。
浮点类型转换
Python float存储为C double。根据IEEE 754转换规则,将Python float转换为C float可能会截断为0.0或正或负无穷大。
complex的类型
Python的complex类型存储为两个双精度的C结构。
Cython具有float complex和doble complex的C级类型,它们对应于Python的complex类型。 C类型具有与Python复杂类型相同的接口,但使用高效的C级操作。这包括访问实部和虚部的实数和图像属性,创建数字的复共轭的共轭方法,以及加法,减法,乘法和除法的有效运算。 C级复杂类型与C99 _Complex类型或C ++ std :: complex模板类兼容。
字节类型
Python字节类型自动转换为char *或std :: string。
str和unicode类型
需要设置c_string_type和c_string_encoding编译器指令(请参阅第66页的“str,unicode,bytes和All That”)以允许str或unicode类型与char *或std :: string进行转换。
到目前为止,使用Python静态类型声明变量,我们已经使用cdef静态声明具有C类型的变量。也可以使用cdef静态声明Python类型的变量。我们可以为list,tuple和dict等内置类型执行此操作;扩展类型,如NumPy数组等。
并非所有Python类型都可以静态声明:它们必须在C中实现,Cython必须能够访问声明。内置的Python类型已经满足了这些要求,并且声明它们非常简单。例如:
此示例中的变量是完整的Python对象。 Cython将它们声明为指向某些内置Python结构类型的C指针。 它们可以像普通的Python变量一样使用,但是被约束为它们声明的类型:
在这里,通过other_particles删除第0个元素也会删除particles的第0个元素,因为它们指向的是同一个列表。other_particles和particles之间的一个区别是particles只能引用Python列表对象,而other_particles可以引用任何Python类型。 Cython将在编译时和运行时对particles强制执行约束。
(补充:在Python内置类型(如int或Float)与C类型相同的情况下,C类型优先。)
当我们添加,减去或乘以标量时,且操作数是动态类型的Python对象时,操作具有Python语义(包括对大值的自动Python长度强制)。当操作数是静态类型的C变量时,它们具有C语义(即,对于有限精度的整数类型,结果可能会溢出)。
除法和模数(即计算余数)值得特别提及。当使用有符号整数操作数计算模数时,C和Python具有明显不同的行为:C向零舍入,而Python向无穷大舍入。例如,-1%5使用Python语义评估为4;然而,而在C语义下,它的结果为-1。当划分两个整数时,Python总是检查分母并在零时引发ZeroDivisionError,而C没有这样的安全措施。Cython默认使用Python语义进行除法和模数,即使操作数是静态类型的C标量。要获得C语义,我们可以在全局模块级别或指令注释中使用cdivision编译器指令(请参见第28页的“编译器指令”):
明显“#”的定义或者是decorator的方式更加方便一些。
注意,当我们用cdivision(True)并且发生除法的时候,如果分母为零,结果可能导致未定义的行为(即,从硬崩溃到损坏的数据的任何事情)。Cython还有cdivision_warnings编译器指令(默认值为False)。 当cdivision_warnings为True时,只要使用负操作数执行除法(或模),Cython就会发出运行时警告。
静态类型的加速
一开始,Cython允许使用内置Python类型静态声明变量,这似乎很奇怪。为什么不像往常一样使用Python的动态类型?答案指向一般的Cython原则:我们提供的静态类型信息越多越好,Cython可以优化静态类型数据的运算速度。与往常一样,这条规则也有例外,但通常情况并非如此。例如,这行代码只是将一个Particle对象附加到动态dynamic_particles变量:
cython编译器将生成可以处理任何Python对象的代码,并在运行时测试dynamic_particles是否为列表。如果不是,只要它有一个带参数的append方法,这个代码就会运行。生成的代码首先在dynamic_particles对象上查找append属性(使用PyObject_GetAttr),然后使用完全通用的PyObject_Call Python / C API函数调用该方法。这基本上模拟了Python解释器在运行等效的Python字节码时所执行的操作。(意思就是说,使用这样的定义方式在效率上和直接在python中运行基本差不多)
假设我们静态声明一个static_particles Python列表并使用它代替:
现在,Cython可以生成专门的代码,直接从C API调用PyList_SET_ITEM或PyList_Append函数。这就是前面示例中的PyObject_Call最终调用的结果,但静态类型允许Cython大大降低static_particles上的动态调度的开销。
Cython目前支持几种内置的静态声明Python类型,包括
将来的版本可能支持更多类型。
具有直接C对象的Python类型(如int,long和float)不包含在前面的列表中。 事实证明,在Cython中静态声明和使用PyIntObjects,PyLongObjects或PyFloatObjects并不简单; 幸运的是,这样做的必要性很少。 我们只是声明常规的C int,long,float和double,让Cython为我们自动转换为Python。
(补充1:!!!! 然而我们之前的文章中实战测试的结果证明,这种cdef list的方式并不是最快的(cimport numpy这个是比较快的),python中的list、tuple等数据类型使用c或者c++的原生的方式来定义会更快,比如python中的list用c++中的vector来代替的话速度能够得到非常大的提高,后面我们实战的时候再慢慢锻炼学习——因为我现在也不会)
(补充2:Python float对应于C double。 出于这个原因,只要使用转换和Python来确保没有截断或者精度损失,C双精度是首选。 在Python 2中,Python int(更确切地说,C级的PyIntObject)在内部将其值存储为C long。 所以C long是首选的整数数据类型,以确保与Python的最大兼容性)
Python在C级别还有一个PyLongObject来表示任意大小的整数。 在Python 2中,它们作为long类型公开,如果PyIntObject的操作溢出,则会产生PyLongObject。在Python 3中,在C级别,所有整数都是PyLongObjects。 Cython以语言无关的方式在C整数类型和这些Python整数类型之间正确转换,并在无法进行转换时引发OverflowError。 当我们在Cython中使用Python对象时,无论是静态声明还是动态,Cython仍然为我们管理对象的所有方面,其中包括引用计数的繁琐。
引用计数和静态字符串类型
Python的主要功能之一是自动内存管理。 CPython通过简单的引用计数实现这一点,使用自动垃圾收集器定期运行以清理无法访问的引用周期。 Cython为我们处理所有引用计数,确保Python对象(无论是静态类型还是动态)在引用计数达到零时完成.CPython的自动内存管理在Cython中混合静态和动态变量时有一定的含义。 比方说,我们有两个Python字节对象b1和b2,我们希望在将它们添加到一起后提取底层字符指针:
b1 + b2表达式是一个临时的Python字节对象,赋值尝试使用Cython的自动转换规则提取该临时对象的char指针。 由于添加的结果是临时对象,前面的示例无法工作 - 添加的临时在创建后立即删除,因此char缓冲区不能引用有效的Python对象。 幸运的是,Cython能够捕获错误并发出编译错误。一旦理解,完成我们想要的正确方法就是直截了当 - 只需使用临时Python变量,动态输入:
这些情况并不常见。 这是一个问题,因为C级对象指的是由Python对象管理的数据。 Python对象拥有底层字符串,所以C char *缓冲区无法告诉Python它有另一个(非Python)引用。 我们必须创建一个临时的字节对象,以便Python不删除字符串数据,只要需要C char *缓冲区,我们必须确保维护临时对象。 表3-2中列出的其他C类型都是值类型,而不是指针类型。 对于这些类型,Python数据在赋值(C语义)期间被复制,允许C变量与用于初始化它的Python对象分开。 就像Cython理解动态Python变量和静态C变量一样也理解两种语言的功能,并允许我们使用任何一种。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。