Python的一个超集,可以编译为C,Cython结合了Python的易用性和原生代码的速度。
Python作为最方便,丰富的配置和彻底有用的编程语言之一而享有盛誉。 但执行速度?没那么快。
让我们开始了解Cython,Cython语言是Python的一个超集,编译成C语言,产生的性能提升可以从几个百分点到几个数量级,具体取决于手头的任务。 对于受Python原生对象类型约束的工作,加速将不会很大。 但是对于数值操作,或任何不涉及Python自身内部的操作,收益可能是巨大的。 这样,许多Python的本地限制可以被绕过或完全超越。
使用Cython,你可以避开Python的许多原生限制,或者完全超越Python,而无需放弃Python的简便性和便捷性。 在本文中,我们将介绍Cython背后的基本概念,并创建一个使用Cython加速功能的简单Python应用程序。
编译Python到C
Python代码可以直接调用C模块。这些C模块可以是通用的C库或专门为Python工作的库。Cython生成第二种类型的模块:与Python内部对话的C库,可以与现有的Python代码绑定在一起。
Cython代码在设计上看起来很像Python代码。如果你给Cython编译器提供了一个Python程序,它将会按原样接受它,但是Cython的原生加速器都不会起作用。但是如果你用Cython的特殊语法来修饰Python代码,那么Cython就可以用快速的C代替慢的Python对象。
请注意,Cython的方法是渐进的。这意味着开发人员可以从现有的Python应用程序开始,通过对代码立刻进行更改来加快速度,而不是从头开始重写整个应用程序。
这种方法通常与软件性能问题的性质相吻合。在大多数程序中,绝大多数CPU密集型代码都集中在一些热点上,也就是帕累托原则的一个版本,也被称为“80/20”规则。因此,Python应用程序中的大部分代码不需要进行性能优化,只需要几个关键部分。你可以逐渐将这些热点转换为Cython,从而获得你最需要的性能提升。程序的其余部分可以保留在Python中,以方便开发人员。
如何使用Cython
下面的代码来自Cython文档:
def f(x): return x**2-xdef integrate_f(a, b, N): s = 0 dx = (b-a)/N for i in range(N): s += f(a+i*dx) return s * dx
这是一个例子,一个不完整的函数的实现。作为纯Python代码,速度很慢,因为Python必须在机器本机数字类型和其内部对象类型之间来回转换。
现在考虑相同代码的Cython版本,并强调Cython的增加:
cdef double f(double x): return x**2-xdef integrate_f(double a, double b, int N): cdef int i cdef double s, x, dx s = 0 dx = (b-a)/N for i in range(N): s += f(a+i*dx) return s * dx
如果我们显式声明变量类型,无论是函数参数还是函数体(double,int等)中使用的变量,Cython都会将所有这些转换成C语言。我们也可以使用cdef关键字来定义 尽管这些函数只能被其他的Cython函数调用,而不能被Python脚本调用,但是这些函数主要是用C实现的。
Cython优势
除了能够加速已经编写的代码之外,Cython还具有其他几个优点:
使用外部C库可以更快
像NumPy这样的Python软件包可以在Python界面中打包C库,使它们易于使用。但是,这些包在Python和C之间来回切换会减慢速度。Cython可以让你直接与底层库进行通信,而不需要Python(也支持C ++库)。
可以同时使用C和Python内存管理
如果你使用Python对象,它们就像在普通的Python中一样被内存管理和垃圾收集。但是如果你想创建和管理自己的C级结构,并使用malloc/free来处理它们,你可以这样做,只记得自己清理一下。
可以根据需要选择安全性或速度
Cython通过decorator 和编译器指令(例如@boundscheck(False))自动执行对C中弹出的常见问题的运行时检查,例如对数组的超出边界访问。因此,由Cython生成的C代码默认比手动C代码安全得多。
如果确信在运行时不需要这些检查,则可以在整个模块上或仅在选择功能上禁用它们以获得额外的编译速度。
Cython还允许本地访问使用“缓冲协议”的Python结构,以直接访问存储在内存中的数据(无需中间复制)。Cython的“记忆视图”可以高速地在这些结构上进行工作,并且具有适合任务的安全级别。
Cython C代码可以从释放GIL中受益
Python的全局解释器锁(Global Interpreter Lock,GIL)同步解释器中的线程,保护对Python对象的访问并管理资源的争用。但GIL被广泛批评为Python性能的绊脚石,特别是在多核系统上。
如果有一段代码不会引用Python对象并执行长时间运行,那么可以使用nogil:指令将其标记为允许它在没有GIL的情况下运行。这使得Python中间人可以做其他事情,并允许Cython代码使用多个内核(附加工作)。
Cython可以使用Python类型的提示语法
Python有一个类型提示语法,主要由linters和代码检查器使用,而不是CPython解释器。 Cython有它自己的代码装饰的自定义语法,但是最近修改了Cython,你可以使用Python类型提示语法为Cython提供类型提示。
Cython限制
请记住,Cython不是一个魔术棒。它不会自动将每一个poky Python代码变成极速的C代码。为了充分利用Cython,你必须明智地使用它,并理解它的局限性:
常规Python代码的加速很少
当Cython遇到Python代码时,它不能完全翻译成C语言,它将这些代码转换成一系列对Python内部的C调用。这相当于将Python的解释器从执行循环中提取出来,这使得代码默认加速了15%到20%。请注意,这是最好的情况。在某些情况下,可能看不到性能改善,甚至性能下降。
原生Python数据结构有一点加速
Python提供了大量的数据结构 - 字符串,列表,元组,字典等等。它们对于开发者来说非常方便,而且他们自带了自动内存管理功能,但是他们比纯C慢。
Cython让你继续使用所有的Python数据结构,尽管没有太多的加速。这又是因为Cython只是在Python运行时调用创建和操作这些对象的C API。因此,Python数据结构的行为与Cython优化的Python代码大致相同:有时会得到一个提升,但只有一点。
Cython代码运行速度最快时,“纯C”
如果你在C中有一个标有cdef关键字的函数,那么它的所有变量和内联函数调用都是纯C的,所以它的运行速度可以和C一样快。 但是,如果该函数引用任何Python原生代码(如Python数据结构或对内部Python API的调用),则该调用将成为性能瓶颈。
幸运的是,Cython提供了一种方法来发现这些瓶颈:一个源代码报告,一目了然地显示您的Cython应用程序的哪些部分是纯C以及哪些部分与Python交互。 对应用程序进行了更好的优化,就会减少与Python的交互。
为Cython应用程序生成的源代码报告。 白色区域纯C;黄色区域显示与Python内部的交互。一个精心优化的Cython程序将尽可能的黄色。 展开的最后一行显示了解释其相应Cython代码的C代码。
Cython NumPy
Cython改进了基于C的第三方数字运算库(如NumPy)的使用。由于Cython代码编译为C,它可以直接与这些库进行交互,并将Python的瓶颈带出循环。
但是NumPy特别适用于Cython。 Cython对NumPy中的特定结构具有本地支持,并提供对NumPy数组的快速访问。在传统的Python脚本中使用的熟悉的NumPy语法可以在Cython中使用。
但是,如果要创建Cython和NumPy之间最接近的绑定,则需要使用Cython的自定义语法进一步修饰代码。例如,cimport语句允许Cython代码在编译时在库中查看C级构造,以实现最快的绑定。
由于NumPy被广泛使用,Cython支持NumPy“开箱即用”。如果你安装了NumPy,你可以在你的代码中声明cimport numpy,然后添加进一步的装饰来使用暴露的函数。
Cython分析和性能
可以通过分析代码并亲眼目睹瓶颈在哪里获得最佳性能。Cython为Python的cProfile模块提供钩子,因此可以使用Python自己的分析工具来查看Cython代码的执行情况。无需在工具组之间切换;可以继续所熟悉和喜爱的Python世界中工作。
它有助于记住所有情况下,Cython不是魔术,仍然适用明智的现实世界的表现实践。在Python和Cython之间来回穿梭越少,你的应用运行得越快。
例如,如果你有一个你想要在Cython中处理的对象的集合,那么不要在Python中迭代它,并且在每一步调用一个Cython函数。将整个集合传递给你的Cython模块并在那里迭代。这种技术经常在管理数据的库中使用,因此这是在自己的代码中模拟的好模型。
我们使用Python是因为它为程序员提供了便利,并且能够快速开发。有时程序员的工作效率是以牺牲性能为代价的。使用Cython,只需要一点点额外的努力就可以给你两全其美的好处。