当前位置:

首页 > 编程开发 > Python、C、C 扩展和Cython之间的差异和比较

Python、C、C 扩展和Cython之间的差异和比较

我们以简单的斐波那契数列为例,来测试一下它们执行效率的差异。Python代码:deffib(n):a,b=0.0,1.0foriinrange(n):a,b=a+b,areturnaC代码:doublecfib(intn){inti;doublea=0.0,b=1.0,tmp;for(i=0;i<n;++i){tmp=a;a=a+b;b=tmp;}returna;}上面便是C

Python、C、C 扩展和Cython之间的差异和比较

我们以简单的斐波那契数列为例,来测试一下它们执行效率的差异。

Python 代码:

def fib(n):
a, b = 0.0, 1.0
for i in range(n):
a, b = a + b, a
return a

C 代码:

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i

上面便是 C 实现的一个斐波那契数列,可能有人好奇为什么我们使用浮点型,而不是整型呢?答案是 C 的整型是有范围的,所以我们使用 double,而且 Python 的 float 在底层对应的是 PyFloatObject、其内部也是通过 double 来存储的。

C 扩展:

然后是 C 扩展,注意:C 扩展不是我们的重点,写 C 扩展和写 Cython 本质是一样的,都是为 Python 编写扩展模块,但是写 Cython 绝对要比写 C 扩展简单的多。

#include "Python.h"

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i

可以看到,如果是写 C 扩展,即便一个简单的斐波那契,都是非常复杂的事情。

Cython 代码:

最后看看如何使用 Cython 来编写斐波那契,你觉得使用 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 代码是不是很相似呢?虽然我们现在还没有正式学习 Cython 的语法,但你也应该能够猜到上面代码的含义是什么。我们使用 cdef 关键字定义了一个 C 级别的变量,并声明了它们的类型。

Cython 代码也是要编译成扩展模块之后,才能被解释器识别,所以它需要先被翻译成 C 的代码,然后再编译成扩展模块。再次说明,写 C 扩展和写 Cython 本质上没有什么区别,Cython 代码也是要被翻译成 C 代码的。

但很明显,写 Cython 比写 C 扩展要简单很多,如果编写的 Cython 代码质量很高,那么翻译出来的 C 代码的质量同样很高,而且在翻译的过程中还会自动进行最大程度的优化。但如果是手写 C 扩展,那么一切优化都要开发者手动去处理,更何况在功能复杂的时候,写 C 扩展本身就是一件让人头疼的事情。

Cython 为什么能够加速?

观察一下 Cython 代码,和纯 Python 的斐波那契相比,我们看到区别貌似只是事先规定好了变量 i、a、b 的类型而已,关键是为什么这样就可以起到加速的效果呢(虽然还没有测试,但速度肯定会提升的,否则就没必要学 Cython 了)。

但是原因就在这里,因为 Python 中所有的变量都是一个泛型指针 PyObject *。PyObject(C 的一个结构体)内部有两个成员,分别是 ob_refcnt:保存对象的引用计数、ob_type *:保存对象类型的指针。

不管是整数、浮点数、字符串、元组、字典,亦或是其它的什么,所有指向它们的变量都是一个 PyObject *。当进行操作的时候,首先要通过 -> ob_type 来获取对应类型的指针,再进行转化。

比如 Python 代码中的 a 和 b,我们知道无论进行哪一层循环,结果指向的都是浮点数,但是解释器不会做这种推断。每一次相加都要进行检测,判断到底是什么类型并进行转化;然后执行加法的时候,再去找内部的 __add__ 方法,将两个对象相加,创建一个新的对象;执行结束后再将这个新对象的指针转成 PyObject *,然后返回。

并且 Python 的对象都是在堆上分配空间,再加上 a 和 b 不可变,所以每一次循环都会创建新的对象,并将之前的对象给回收掉。

以上种种都导致了 Python 代码的执行效率不可能高,虽然 Python 也提供了内存池以及相应的缓存机制,但显然还是架不住效率低。

至于 Cython 为什么能加速,我们后面会慢慢聊。

效率差异

那么它们之间的效率差异是什么样的呢?我们用一个表格来对比一下:

Python、C、C 扩展和Cython之间的差异和比较

提升的倍数,指的是相对于纯 Python 来说在效率上提升了多少倍。

第二列是 fib(0),显然它没有真正进入循环,fib(0) 测量的是调用一个函数所需要花费的开销。而倒数第二列 "循环体耗时" 指的是执行 fib(90) 的时候,排除函数调用本身的开销,也就是执行内部循环体所花费的时间。

整体来看,纯 C 语言编写的斐波那契,毫无疑问是最快的,但是这里面有很多值得思考的地方,我们来分析一下。

纯 Python

众望所归,各方面都是表现最差的那一个。从 fib(0) 来看,调用一个函数要花 590 纳秒,和 C 相比慢了这么多,原因就在于 Python 调用一个函数的时候需要创建一个栈帧,而这个栈帧是分配在堆上的,而且结束之后还要涉及栈帧的销毁等等。至于 fib(90),显然无需分析了。

纯 C

显然此时没有和 Python 运行时的交互,因此消耗的性能最小。fib(0) 表明了,C 调用一个函数,开销只需要 2 纳秒;fib(90) 则说明执行一个循环,C 比 Python 快了将近80倍。

C 扩展

C 扩展是干什么的上面已经说了,就是使用 C 来为 Python 编写扩展模块。我们看一下循环体耗时,发现 C 扩展和纯 C 是差不多的,区别就是函数调用上花的时间比较多。原因就在于当我们调用扩展模块的函数时,需要先将 Python 的数据转成 C 的数据,然后用 C 函数计算斐波那契数列,计算完了再将 C 的数据转成 Python 的数据。

所以 C 扩展本质也是 C 语言,只不过在编写的时候还需要遵循 CPython 提供的 API 规范,这样就可以将 C 代码编译成 pyd 文件,直接让 Python 来调用。从结果上看,和 Cython 做的事情是一样的。但是还是那句话,用 C 写扩展,本质上还是写 C,而且还要熟悉底层的 Python/C API,难度是比较大的。

Cython

单独看循环体耗时的话,纯 C 、C 扩展、Cython 都是差不多的,但是编写 Cython 显然是最方便的。而我们说 Cython 做的事情和 C 扩展本质是类似的,都是为 Python 提供扩展模块,区别就在于:一个是手动写 C 代码,另一个是编写 Cython 代码、然后再自动翻译成 C 代码。所以对于 Cython 来说,将 Python 的数据转成 C 的数据、进行计算,然后再转成 Python 的数据返回,这一过程也是无可避免的。

但是我们看到 Cython 在函数调用时的耗时相比 C 扩展却要少很多,主要是 Cython 生成的 C 代码是经过高度优化的。不过说实话,函数调用花的时间不需要太关心,内部代码块执行所花的时间才是我们需要注意的。当然啦,如何减少函数调用本身的开销,我们后面也会说。

Python 的 for 循环为什么这么慢?

通过循环体耗时我们看到,Python 的 for 循环真的是出了名的慢,那么原因是什么呢?来分析一下。

1. Python 的 for 循环机制

Python 在遍历一个可迭代对象的时候,会先调用可迭代对象内部的 __iter__ 方法返回其对应的迭代器;然后再不断地调用迭代器的 __next__ 方法,将值一个一个的迭代出来,直到迭代器抛出 StopIteration 异常,for 循环捕捉,终止循环。

而迭代器是有状态的,Python 解释器需要时刻记录迭代器的迭代状态。

2. Python 的算数操作

这一点我们上面其实已经提到过了,Python 由于自身的动态特性,使得其无法做任何基于类型的优化。

比如:循环体中的 a + b,这个 a、b 指向的可以是整数、浮点数、字符串、元组、列表,甚至是我们实现了魔法方法 __add__ 的类的实例对象,等等等等。

尽管我们知道是浮点数,但是 Python 不会做这种假设,所以每一次执行 a + b 的时候,都会检测其类型到底是什么?然后判断内部是否有 __add__ 方法,有的话则以 a 和 b 为参数进行调用,将 a 和 b 指向的对象相加。计算出结果之后,再将其指针转成 PyObject * 返回。

而对于 C 和 Cython 来说,在创建变量的时候就事先规定了类型为 double,不是其它的,因此编译之后的 a + b 只是一条简单的机器指令。这对比下来,Python 尼玛能不慢吗。

3. Python 对象的内存分配

Python 的对象是分配在堆上面的,因为 Python 对象本质上就是 C 的 malloc 函数为结构体在堆区申请的一块内存。在堆区进行内存的分配和释放需要付出很大的代价,而栈则要小很多,并且它是由操作系统维护的,会自动回收,效率极高,栈上内存的分配和释放只是动一动寄存器而已。

但堆显然没有此待遇,而恰恰 Python 的对象都分配在堆上,尽管 Python 引入了内存池机制使得其在一定程度上避免了和操作系统的频繁交互,并且还引入了小整数对象池、字符串的intern机制,以及缓存池等。

但事实上,当涉及到对象(任意对象、包括标量)的创建和销毁时,都会增加动态分配内存、以及 Python 内存子系统的开销。而 float 对象又是不可变的,因此每循环一次都会创建和销毁一次,所以效率依旧是不高的。

而 Cython 分配的变量(当类型是 C 里面的类型时),它们就不再是指针了(Python 的变量都是指针),对于当前的 a 和 b 而言就是分配在栈上的双精度浮点数。而栈上分配的效率远远高于堆,因此非常适合 for 循环,所以效率要比 Python 高很多。另外不光是分配,在寻址的时候,栈也要比堆更高效。

所以在 for 循环方面,C 和 Cython 要比纯 Python 快了几个数量级,这并不是奇怪的事情,因为 Python 每次迭代都要做很多的工作。

什么时候使用 Cython?

我们看到在 Cython 代码中,只是添加了几个 cdef 就能获得如此大的性能改进,显然这是非常让人振奋的。但是,并非所有的 Python 代码在使用 Cython 编写时,都能获得巨大的性能改进。

我们这里的斐波那契数列示例是刻意的,因为里面的数据是绑定在 CPU 上的,运行时都花费在处理 CPU 寄存器的一些变量上,而不需要进行数据的移动。如果此函数做的是如下工作:

  • 内存密集,比如给大数组添加元素;
  • I/O 密集,比如从磁盘读取大文件;
  • 网络密集,比如从 FTP 服务器下载文件;

那么 Python,C,Cython 之间的差异可能会显著减少(对于存储密集操作),甚至完全消失(对于 I/O 密集或网络密集操作)。

当提升 Python 程序性能是我们的目标时,Pareto 原则对我们帮助很大,即:程序百分之 80 的运行耗时是由百分之 20 的代码引起的。但如果不进行仔细的分析,那么是很难找到这百分之 20 的代码的。因此我们在使用 Cython 提升性能之前,分析整体业务逻辑是第一步。

如果我们通过分析之后,确定程序的瓶颈是由网络 IO 所导致的,那么我们就不能期望 Cython 可以带来显著的性能提升。因此在你使用 Cython 之前,有必要先确定到底是哪种原因导致程序出现了瓶颈。所以尽管 Cython 是一个强大的工具,但前提是它必须应用在正确的道路上。

另外 Cython 将 C 的类型系统引入进了 Python,所以 C 的数据类型的限制是我们需要关注的。我们知道,Python 的整数不受长度的限制,但是 C 的整数是受到限制的,这意味着它们不能正确地表示无限精度的整数。

不过 Cython 的一些特性可以帮助我们捕获这些溢出,总之最重要的是:C 数据类型的速度比 Python 数据类型快,但是会受到限制导致其不够灵活和通用。从这里我们也能看出,在速度以及灵活性、通用性上面,Python 选择了后者。

此外,思考一下 Cython 的另一个特性:连接外部代码。假设我们的起点不是 Python,而是 C 或者 C++,我们希望使用 Python 将多个 C 或者 C++ 模块进行连接。而 Cython 理解 C 和 C++ 的声明,并且它能生成高度优化的代码,因此更适合作为连接的桥梁。

由于我本人是主 Python 的,如果涉及到 C、C++,都是介绍如何在 Cython 中引入 C、C++,直接调用已经写好的 C 库。而不会介绍如何在 C、C++ 中引入 Cython,来作为连接多个 C、C++ 模块的桥梁。这一点望理解,因为本人不用 C、C++ 编写服务,只会用它们来辅助 Python 提高效率。

小结

到目前为止,只是介绍了一下 Cython,并且主要讨论了它的定位,以及和 Python、C 之间的差异。至于如何使用 Cython 加速 Python,如何编写 Cython 代码、以及它的详细语法,我们将会后续介绍。

总之,Cython 是一门成熟的语言,它是为 Python 而服务的。Cython 代码不能够直接拿来执行,因为它不符合 Python 的语法规则。

我们使用 Cython 的方式是:先将 Cython 代码翻译成 C 代码,再将 C 代码编译成扩展模块(pyd 文件),然后在 Python 代码中导入它、调用里面的功能方法,这是我们使用 Cython 的正确途径、当然也是唯一的途径。

比如我们上面用 Cython 编写的斐波那契,如果直接执行的话是会报错的,因为 cdef 明显不符合 Python 的语法规则。所以 Cython 代码需要编译成扩展模块,然后在普通的 py 文件中被导入,而这么做的意义就在于可以提升运行速度。因此 Cython 代码应该都是一些 CPU 密集型的代码,不然效率很难得到大幅度提升。

所以在使用 Cython 之前,最好先仔细分析一下业务逻辑,或者暂时先不用 Cython,直接完全使用 Python 编写。编写完成之后开始测试、分析程序的性能,看看有哪些地方耗时比较严重,但同时又是可以通过静态类型的方式进行优化的。找出它们,使用 Cython 进行重写,编译成扩展模块,然后调用扩展模块里面的功能。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发 Python
相关文章 更多
Python安装后怎么打开:使用IDLE或命令行启动解释器
Python安装后怎么打开:使用IDLE或命令行启动解释器

刚在Windows安装好Python却不知道如何启动?本文详细演示如何通过开始菜单找到并打开IDLE集成开发环境,以及如何在PowerShell或命令提示符中使用python和py命令启动交互式解释器、运行.py脚本文件。包含退出解释器的方法及常见启动问题排查,帮助初学者快速验证安装成功并开始编写代码。

Windows系统Python安装教程:下载、勾选PATH及环境变量配置
Windows系统Python安装教程:下载、勾选PATH及环境变量配置

针对Windows初学者的Python安装实战指南。详细讲解如何从Python官网下载匹配架构的安装包,重点演示安装首屏勾选“Add python.exe to PATH”的关键操作,并提供使用python --version和py命令验证环境变量的具体步骤,帮助新手快速搭建开发环境并排查路径问题。

麒麟OS如何查看Python进程的运行状态
麒麟OS如何查看Python进程的运行状态

要想确认麒麟OS中Python程序的运行状态以及资源占用情况,我们可以这样做:用ps -ef | grep python来筛选进程;通过top命令,按P键排序查看实时负载;使用pgrep -f "script.py"精准获取PID;借助lsof -p PID验证文件打开状态。另外,还可以结合syst

Python在Debian上如何配置SSL证书
Python在Debian上如何配置SSL证书

在Debian系统上配置SSL证书通常涉及以下几个步骤:安装Web服务器:首先,你需要一个Web服务器,比如Apache或Nginx。这里以Apache为例。sudo apt updatesudo apt install apache2获取SSL证书:你可以从Let’s Encrypt免费获取SSL

统信UOS怎么安装Python开发环境
统信UOS怎么安装Python开发环境

要想让Python项目在统信UOS上正常运行,得先安装python3、python3-pip、python3-venv、python3-dev以及build-essential等组件。具体操作就是执行sudo apt install命令来一步到位完成安装,同时别忘了配置清华镜像源来给pip加速哦。在

纯Python方案实现中英文全文搜索
纯Python方案实现中英文全文搜索

在互联网上的各类网站中,无论大小,基本上都会有一个搜索框,用来给用户对内容进行搜索,小到站点搜索,大到搜索引擎搜索。从简单的来说,搜索功能确实很简单,一个简单的select语句就可以实现数据的搜索。而从复杂的来看,无论是搜索的精度还是搜索的效率,都是有很深的研究范围的。对于简单的搜索功能来说,一个s

Mac如何取消通过Python脚本运行的关机程序
Mac如何取消通过Python脚本运行的关机程序

立即在终端输入sudo shutdown -c取消倒计时关机,成功后显示“Shutdown cancelled”;若存在pmset重复任务,需再执行sudo pmset repeat cancel清除。Mac因Python脚本执行了os.system("sudo shutdown -h +10")或

Pythonasyncio异步并发与多固定出口IP调度实战
Pythonasyncio异步并发与多固定出口IP调度实战

之前写过一篇同步场景下用 Python 管理多个固定出口 IP 的实践(ExitPool + requests/httpx),覆盖了健康检查、故障转移和连接池复用。但在实际业务中,越来越多的场景用 asyncio 做高并发采集或批量接口调用——异步事件循环下多出口的管理方式和同步场景完全不同:单线程

Python在静态出口IP产品中的实战:从地址漂移巡检到多IP故障切换
Python在静态出口IP产品中的实战:从地址漂移巡检到多IP故障切换

写在前面:为什么静态出口 IP 不是"买了就行"不少团队在引入静态出口 IP 产品时,第一反应往往是:“地址配上去,这事就算完了。”可真到了真实业务里,静态出口 IP 真正能体现价值的地方,往往不在分配这一步,而在分配之后怎么管:地址有没有漂移,质量是否达标,某一条线路突然不可用时怎么切换,连接层又

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。