Cython 和 Python 的区别

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Cython 和 Python 的区别


楔子




前面我们说了 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 模块,但是里面的内容已经是经由标准 C 编译器编译成的机器码。Python 虚拟机在导入扩展模块执行的时候,不会再解释高级字节码,而是直接运行机器代码,这样就能移除性能开销。

这里再提一下扩展模块,我们说 Windows 中存在 .dll(动态链接库)、Linux 中存在 .so(共享文件)。如果只是 C 或者 C++、甚至是 Go 等等编写的普通源文件,然后编译成 .dll 或者 .so,那么这两者可以通过 ctypes 调用,但是无法通过 import 导入。如果你强行导入,那么会报错:

ImportError: dynamic module does not define module export function

但如果是遵循 Python/C API 编写,尽管编译出的扩展模块在 Linux 上也是 .so、Windows 上是 .pyd(.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 这种静态语言,可以创建一个变量的同时不赋上初始值,比如:int n,因为已经知道 n 是一个 int 类型了,所以分配的空间大小已经确定了。

并且对于动态语言来说,变量即使在同一个作用域中,也可以指向任意的对象,因为变量只是一个指针罢了。举个栗子:

var = 666
var = "古明地觉"

首先是 var = 666,相当于创建了一个整数 666,然后让 var 这个变量指向它;

再来一个 var = "古明地觉",那么会创建一个字符串,然后让 var 指向这个字符串。或者说 var 不再存储整数 666 的地址,而是存储新创建的字符串的地址。

所以在运行 Python 程序时,解释器要花费很多时间来确认执行的低阶操作,并抽取相应的数据。考虑到 Python 设计的灵活性,解释器总是要以一种非常通用的方式来确定相应的低阶操作,因为 Python 的变量在任意时刻可以指向任意类型的数据。以上便是所谓的动态解析,而 Python 的通用动态解析是缓慢的,还是以 a + b 为栗:

1)解释器要检测 a 指向的对象的类型,这在 C 一级至少需要一次指针查找;

2)解释器从该类型中寻找加法方法的实现,这可能又需要一个或者多个额外的指针查找和内部函数调用;

3)如果解释器找到了相应的方法,那么解释器就有了一个实际的函数调用;

4)解释器会调用这个加法函数,并将 a 和 b 作为参数传递进去;

5)Python 的对象在 C 中都是一个结构体,比如:整数在 C 中是 PyLongObject,内部有引用计数、类型、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 的出现则简化了这一点,可以让我们在写类似于 Python 代码的同时,还能使用 C 的静态类型系统。

那么下一篇文章,我们就来学习 Cython 的第一个、也是最重要的关键字:cdef,它是我们通往 C 性能的大门。

相关文章
|
1月前
|
存储 大数据 数据处理
Python 中的列表推导式与生成器:特性、用途与区别
Python 中的列表推导式与生成器:特性、用途与区别
20 2
|
1月前
|
存储 C语言 Python
解密 Python 的变量和对象,它们之间有什么区别和联系呢?
解密 Python 的变量和对象,它们之间有什么区别和联系呢?
20 2
|
2月前
|
存储 C语言 Python
通过 Cython 带你认清 Python 变量的本质
通过 Cython 带你认清 Python 变量的本质
50 8
|
2月前
|
存储 Python
Python中类方法、实例方法与静态方法的区别
这三种方法的正确使用可以使代码更加清晰、组织良好并且易于理解,从而有效地支持软件开发的面向对象编程范式。
27 1
|
29天前
|
机器学习/深度学习 缓存 Linux
python环境学习:pip介绍,pip 和 conda的区别和联系。哪个更好使用?pip创建虚拟环境并解释venv模块,pip的常用命令,conda的常用命令。
本文介绍了Python的包管理工具pip和环境管理器conda的区别与联系。pip主要用于安装和管理Python包,而conda不仅管理Python包,还能管理其他语言的包,并提供强大的环境管理功能。文章还讨论了pip创建虚拟环境的方法,以及pip和conda的常用命令。作者推荐使用conda安装科学计算和数据分析包,而pip则用于安装无法通过conda获取的包。
55 0
|
2月前
|
Python
全网最适合入门的面向对象编程教程:Python函数方法与接口-函数与方法的区别和lamda匿名函数
【9月更文挑战第15天】在 Python 中,函数与方法有所区别:函数是独立的代码块,可通过函数名直接调用,不依赖特定类或对象;方法则是与类或对象关联的函数,通常在类内部定义并通过对象调用。Lambda 函数是一种简洁的匿名函数定义方式,常用于简单的操作或作为其他函数的参数。根据需求,可选择使用函数、方法或 lambda 函数来实现代码逻辑。
|
2月前
|
机器学习/深度学习 人工智能 安全
python和Java的区别以及特性
Python:适合快速开发、易于维护、学习成本低、灵活高效。如果你需要快速上手,写脚本、数据处理、做点机器学习,Python就是你的首选。 Java:适合大型项目、企业级应用,性能要求较高的场景。它类型安全、跨平台能力强,而且有丰富的生态,适合更复杂和规模化的开发。
47 3
|
2月前
|
存储 缓存 API
比较一下 Python、C、C 扩展、Cython 之间的差异
比较一下 Python、C、C 扩展、Cython 之间的差异
35 0
|
2月前
|
Python
Python中类属性与实例属性的区别
了解这些区别对于编写高效、易维护的Python代码至关重要。正确地使用类属性和实例属性不仅能帮助我们更好地组织代码,还能提高代码运行的效率。
26 0
|
3月前
|
存储 测试技术 Python
Python 数组和列表有什么区别?
【8月更文挑战第29天】
382 4