为什么Python相较于其它主流语言运行要慢?

Python现在越来越火,已经迅速扩张到了包括DevOps、数据科学、Web开发、信息安全等各个领域当中。

然而,相比起Python的扩张速度,Python代码的运行速度就显得有点逊色了。

在代码运行速度方面,Java、C、C++、C#和Python要如何进行比较呢?并没有一个放之四海而皆准的标准,因为具体结果很大程度上取决于运行的程序类型,而语言基准测试Computer Language Benchmarks Games可以作为衡量的一个方面。

根据这些年语言基准测试的经验结果来看,Python比很多语言运行起来都要慢。无论是使用JIT编译器的C#、Java,还是使用AOT编译器的C、C++,又或者是JavaScript这些解释型语言,Python都比它们运行的慢。

注意:对于文中的“Python”,一般指的是CPython这个官方的实现。当然我也会在本文中提到其它语言的Python实现。

我要回答的是这个问题:对于一个类似的程序,Python要比其它语言慢2到10倍不等,这其中的原因是什么?又有没有什么改善的方法呢?

现在主流的说法有这些:

  • 是全局解释其锁Global Interpreter Lock(GIL)的原因
  • 是因为Python是解释型语言而不是编译型语言
  • 是因为Python是一种动态类型的语言

那么到底哪一个才是影响Python运行效率的原因呢?

是全局解释器所的原因吗?

现在很多计算机都配备了具有多个核的CPU,有时甚至还会有多个处理器。
为了更充分的利用它们的处理能力,操作系统定义了一个称为线程的低级结构。
某一个进程(例如Chrome浏览器)可以建立多个线程,在系统内执行不同的操作。
在这种情况下,CPU密集型进程就可以跨核心分担负载了,这样的做法可以大大提高应用程序的运行效率。

例如在写这篇文章时,我的Chrome浏览器打开了44个线程。
需要提及的是,基于POSIX的操作系统(例如Mac OS、Linux)和Windows操作系统的线程结构、API都是不同的,因此操作系统还负责对各个线程的调度

如果你还没有写过多线程执行的代码,你就需要了解一下线程锁的概念了。
多线程进程比单线程进程更为复杂,是因为需要使用线程锁来确保同一个内存地址中的数据不会被多个线程同时访问或更改

CPython解释器在创建变量时,首先会分配内存,然后对该变量的引用进行计数,这称为引用计数(reference counting)。
如果变量的引用数变为0,这个变量就会从内存中被释放掉。这就是在for循环代码块内创建临时变量不会增加内存消耗的原因。

而当多个线程共享一个变量时,CPython锁定引用计数的关键就在于使用了GIL,它会谨慎的控制线程的执行情况,无论同时存在多少个线程,解释器每次只允许一个线程进行操作。

这会对Python程序的性能产生什么影响?

如果你的程序只有单线程、单进程,代码的速度和性能不会受到全局解释器锁的影响。

但如果你通过在单进程中使用多线程实现并发,并且是IO密集型(例如网络IO或磁盘IO)的线程,GIL竞争的效果就很明显了。
由 David Beazley 提供的 GIL 竞争情况图

对于一个Web应用(例如Django),同时还使用了WSGI,那么对这个Web应用的每一个请求都运行一个单独的Python解释器,而且每个请求只有一个锁。同时因为Python解释器的启动比较慢,某些WSGI实现还具有“守护进程模式”,可以使Python进程一直就绪。

其它的Python解释器表现如何?

PyPy也是一种带有GIL的解释器,但通常比CPython要快3倍以上。

Jython则是一种没有GIL的解释器,这是因为Jython中的Python线程使用的是Java线程来实现的,并且由JVM内存管理系统来进行管理。

JavaScript在这方面又是怎样做的呢?

所有的JavaScript引擎使用的都是mark-and-sweep垃圾收集算法,而GIL使用的则是CPython的内存管理算法。

JavaScript没有GIL,而且它是单线程的,也不需要用到GIL,JavaScript的时间循环和Promise/Callback模式实现了以异步编程的方式代替并发。在Python当中也有一个类似的asyncio事件循环。

是因为Python是解释型语言吗?

我经常会听到这个说法,但是这过于粗陋的简化了Python所实际做的工作了。
其实当终端上执行 python myscript.py 之后,CPython会对代码进行一系列的读取、语法分析、解析、编译、解释和执行的操作。

而且.pyc文件的创建是这个过程的重点。
在代码编译阶段,Python3会将字节码序列写入__pycache__/下的文件中,而Python2则会将字节码序列写入当前目录的.pyc文件中。
对于你编写的脚本、导入的所有代码以及第三方模块都是如此。

因此,绝大多数情况下(除非你的代码是一次性的…),Python都会解释字节码并本地执行。
与Java、C#、.NET相比:
Java代码会被编译为“中间语言”,有Java虚拟机读取字节码,并将其即时编译为机器码。.NET的CIL(Common-Language-Runtime)也是如此,它将字节码即时编译为机器码。

既然Python像Java和C#那样都是用虚拟机或某种字节码,为什么Python在基准测试中仍然比Java和C#慢得多呢?
首要原因是,.NET和Java都是JIT编译的。

即时编译Just-In-Time(JIT)需要一种中间语言,以便将代码拆分为多个块(或多个帧)。而提前编译Ahead-Of-Time(AOT)则需要确保CPU在任何交互发生之前理解每一行代码。

JIT本身不会使执行速度加快,因为它执行的仍然是同样的字节码序列。但是JIT会允许在运行时进行优化。
一个优秀的JIT优化器会分析出程序的那些部分会被多次执行,这就是程序中的“热点”,然后优化器会将这些代码替换为更有效率的版本以实现优化。

这就意味着如果你的程序是多次重复相同的操作时,有可能会被优化器优化的更快。而且,Java和C#是强类型语言,因此优化器对代码的判断可以更为准确。

PyPy使用了明显快于CPython的JIT。

那为什么CPython不使用JIT呢?

JIT也不是完美的,它的一个显著的缺点就在于启动时间。
CPython的启动时间已经相对比较慢了,而PyPy比CPython启动还要慢2到3倍。
Java虚拟机启动速度也是出了名的慢。.NET的CLR则通过在系统启动时启动来优化体验,而CLR的开发者也是在CLR上开发该操作系统。

因此如果你有个长时间运行的单一Python进程,JIT就比较有意义了,因为代码里有“热点”可以优化。
不过,CPython是个通用的实现。设想如果使用Python开发命令行程序,但每次调用CLI时都必须等待JIT缓慢启动,这种体验就相当不好了。

CPython试图用于各种使用情况。有可能实现将JIT插入到CPython中,但这个改进工作的进度基本处于停滞不前的状态。
如果你想充分发挥JIT的优势,请使用PyPy。

是因为Python是一种动态类型的语言吗?

在C、C++、Java、C#、Go这些静态类型语言中,必须在声明变量时指定变量的类型。而在动态类型语言中,虽然也有类似的概念,但变量的类型是可以改变的。
例如:

a = 1  
a = 'foo'  

在上面这个实例里,Python将变量a一开始存储整数类型变量的内存空间释放了,并创建了一个新的存储字符串类型的内存空间,并且和原来的变量同名。

静态类型语言这样的设计并不是为难你,而是为了方便CPU运行而这样设计的。
因为最终都需要将所有操作都对应为简单的二进制操作,因此必须将对象、类型这些高级的数据结构转换为低级数据结构。

Python也实现了这样的转换,但用户看不到这些转换,也不需要关心这些转换。

不用必须声明类型并不是为了使Python运行慢,Python的设计是让用户可以让各种东西变得动态:可以在运行时更改对象上的方法,也可以在运行时动态添加底层系统调用到值的声明上,几乎可以做到任何事。

但也正是这种设计使得Python的优化异常的难。

为了证明我的观点,我使用了一个Mac OS上的系统调用跟踪工具DTrace。CPython发布版本中没有内置DTrace,因此必须重新对CPython进行编译。下面以Python3.6.6为例:

wget https://github.com/python/cpython/archive/v3.6.6.zip  
unzip v3.6.6.zip  
cd v3.6.6  
./configure --with-dtrace  
make  

这样python.exe将使用DTrace追踪所有代码。Paul Ross也做过关于DTrace的闪电演讲。你可以下载Python的DTrace启动文件来查看函数调用、执行时间、CPU时间、系统调用,以及各种其它内容的试验。

sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py'  

py_callflow追踪器显示了程序里调用的所有函数。
那么,Python的动态类型会让它变慢吗?

  • 类型比较和类型转换消耗的资源时比较多的,每次读取、写入或引用变量时都会检查变量的类型
  • Python的动态程度让它难以被优化,因此很多Python的替代品能够如此快都是为了提升速度而在灵活性上作出了妥协
  • 而CPython结合了C的静态类型和Python来优化已知类型的代码,它可以将性能提升84倍

总结

由于Python是一种动态、多功能的语言,因此运行起来会相对缓慢。对于不同的实际需求,可以使用各种不同的优化或替代方案。

例如可以使用异步编程,引入分析工具或使用多种解释器来优化Python程序。
对于不要求启动时间且代码可以充分利用JIT的程序,可以考虑使用PyPy。
而对于看重性能并且静态类型变量较多的程序,不妨使用Cython。

本文转自:

https://linux.cn/article-10161-1.html
编译:linux中国-Hank Chow,英文:Anthony Shaw

猜你喜欢

转载自blog.csdn.net/june_young_fan/article/details/83547759