为什么Python这么慢?
作者:网友投稿 时间:2018-08-19 21:17

大数据文摘出品
编译:jojo、Hope、张秋玥、CoolBoy
Python语言近年来人气爆棚。它广泛应用于网络开发运营,数据科学,网络开发,以及网络安全问题中。
然而,Python在速度上完全没有优势可言。
在速度上,Java如何同C,C++,C#或者Python相比较?答案几乎完全取决于要运行的应用。在这个问题上,没有完美的评判标准,然而The Computer Language Benchmarks Game 是一个不错的方法。
链接:
基于我对The Computer Language Benchmarks Game超过十年的观察,相比于Java,C#,Go,JavaScript, C++等,Python是最慢的语言之一。其中包括了 JIT (C#, Java) 和 AOT (C, C++)编译器,以及解释型语言,例如JavaScript。
动态编译:https://en.wikipedia.org/wiki/Just-in-time_compilation
静态编译:https://en.wikipedia.org/wiki/Ahead-of-time_compilation
注意:当我提到“Python”时,我指的是CPython这个官方的解释器。我也将在本文中提及其他的解释器。
我想要回答这样一个问题:当运行同一个程序时,为什么Python会 比其他语言慢2到10倍?为什么我们无法将它变得更快?
以下是最主要的原因:
“它是GIL(Global Interpreter Lock全局解释器锁)”
“它是解释型语言而非编译语言”
“它是动态类型语言”
那么以上哪种原因对性能影响最大呢?
“它是全局解释器锁”
现代计算机的CPU通常是多核的,并且有些拥有多个处理器。为了充分利用多余的处理能力,操作系统定义了一种低级的结构叫做线程:一个进程(例如Chrome浏览器)可以产生多个线程并且指导内部系统。
如果一个进程是CPU密集型,那么其负载可以被多核同时处理,从而有效提高大多数应用的速度。
当我写这篇文章时,我的Chrome浏览器同时拥有44个线程。注意,基于POSIX(比如MacOS和Linux)和Windows操作系统相比,线程的结构和API是不同的。操作系统也会处理线程的调度问题。
如果你之前没有做过多线程编程,你需要快速熟悉锁的概念。区别于单线程进程,你需要确保当内存中的变量被修改时,多线程不会同时试图访问或者改变同一个存储地址。
当CPython创建变量时,它会预先分配存储空间,然后计算当前变量的引用数目。这个概念被称为引用计数。如果引用计数为零,那么它将从系统中释放对应存储区域。
这就是为什么在CPython中创造“临时”变量不会使应用占用大量的存储空间——尤其是当应用中使用了for循环这一类可能大量创建“临时”变量的结构时。
当存在多个线程调用变量时,CPython如何锁住引用计数成为了一个挑战。而“全局解释锁”应运而生,它能够谨慎控制线程的执行。无论有多少的线程,解释器每次只能执行一个操作。
这对Python的性能意味着什么呢?
如果你的应用基于单线程、单解释器,那么讨论速度这一点就毫无意义,因为去掉GIL并不会影响代码性能。
如果你想使用线程在单解释器(Python 进程)中实现并发,并且你的线程为IO密集型(例如网络IO或磁盘IO),你就会看到GIL争用的结果。

该图来自David Beazley的GIL可视化
如果你有一个网络应用(例如Django)并且使用WSGI,那么每一个对于你的网络应用的请求将是一个独立的Python解释器,因此每个请求只有一个锁。因为Python解释器启动很慢,一些WSGI便集成了能够使保持Python进程的“守护进程” 。
那么其他Python解释器的速度又如何呢?
PyPy拥有GIL,通常比CPython快至少三倍。
Jython没有GIL,因为在Jython中Python线程是用Java线程表示的,这得益于JVM内存管理系统。
JavaScript是如何做到这一点的呢?
首先,所有的Javascript引擎使用标记加清除的垃圾收集系统,而之前提到GIL的基本诉求是CPython的存储管理算法。
JavaScript没有GIL,但因为它是单线程的,所以也并不需要GIL。
JavaScript通过事件循环和承诺/回调模式来实现异步编程的并发。Python有与异步事件循环相似的过程。
“因为它是解释型语言”
我经常听到这句话。我觉得这只是对于CPython实际运行方式的一种简单解释。如果你在终端中输入python myscript.py,那么CPython将对这段代码开始一系列的读取,词法分析,解析,编译,解释和运行。



