Python如今非常流行,它被广泛用于DevOps,数据科学,web开发以及安全等方面。
尽管如此,Python并没有在运行速度方面获得过任何的赞誉。与c , c++ , c# 或 python相比,java的运行速度如何呢? 这个问题的答案取决于你在运行什么类型的程序。没有哪一个用于比较速度的基准是完美的,但使用编程语言基准游戏(The Computer Language Benchmarks Game)是一个不错的开始。
我参考了近十年来编程语言基准游戏的结果,发现与其他编程语言相比,Python是最慢的之一。这些编程语言包括了即时编译型语言(C#, Java),提前编译型语言(C, C++)和JavaScript这种解释型语言。
注:当我说 "python" 时, 我说的是它语言的参考实现——CPython。在本文中,我还将提到它其他的参考实现。
我想回答这个问题:“Python执行一个程序比其他编程语言慢2-10倍,为什么它这么慢?我们就不能让它快点吗?”
以下是一些常见的论调:“是因为全局解释器锁(GIL,Global Interpreter Lock)。”
“因为它是一种解释型语言,并没有编译。”
“因为它是一种动态类型的语言。”
那么哪一个原因才是对它的性能产生最大影响的呢?
1. “因为全局解释器锁”
如果你以前没有从事过多线程编程,那么有一个概念你需要尽快熟悉——锁。与单线程编程不同,你需要确保当改变内存中的变量时,多个线程不会在同一时间访问或更改同一个内存地址。
当CPython创建一个变量的时候,它会分配内存地址,然后计算有多少个指向这个变量的引用存在,这是一种被称作“引用计数”的技术。当被引用的数量是0时,这个变量就会从内存中释放。这也就是为什么你在循环体内创建一个”临时“变量的时候,并不会让你的应用程序的内存消耗猛增。
所以挑战就变成了当多个线程共享一个变量的时候,CPython是如何锁定引用计数的?有一个全局解释器锁仔细地控制着线程的执行,这个解释器每一次只能执行一个操作,而不管有多少个线程。
1.1 对于Python应用程序来说,这意味着什么?
如果你的程序是单线程的,单解释器的,那对你的速度并没有任何差别。移除GIL对你的程序的性能也没有任何影响。
如果你想用多线程在一个解释器中实现并发性,并且你的线程是IO密集的(例如,网络IO或磁盘IO),你就会看到GIL竞争资源的后果。
如果你的程序是一个网络应用程序(例如,使用了Django),并且你用了WSGI,那么每个给你的网络应用程序发送的请求都是一个单独的Python解释器,所以每个请求都有一个锁。因为Python解释器启动很慢,因此某些 WSGI的实现具有 "守护进程模式",它能保持Python进程启动着,并且随时待命。
1.2 Python其他的具体实现怎么样?
PyPy也有GIL,但它比 CPython 快了3倍以上。
Jython没有GIL,因为在 Jython 中的 Python线程由 Java线程表示, 并受益于 JVM的内存管理系统,使得它并不需要GIL。
1.3 JavaScript 是怎么做的呢?
首先,所有的JavaScript 引擎都使用标记和扫描垃圾回收机制,而正如我们上面提到的那样,需要GIL的最重要原因是CPython那样的内存管理算法。
JavaScript 没有GIL,但因为它是单线程的,所以不需要GIL。JavaScript 的事件循环和Promise/Callback模式是用异步编程替代了并发性。在Python中,使用 asyncio 模块中的事件循环也能做到类似的事情。
2. “因为它是一门解释型语言”
我常常听别人这样说,但我觉得这是对CPython真正的工作方式的一种草率的简化。当你在你的终端写下myscript.py文件,CPython会进行以下一系列的工作:读取、词法分析、语法分析、编译、解释,然后运行这些代码。
如果你对这个过程感兴趣,可以参考我之前写的一篇文章《6分钟内改造Python语言》。
一个关键点就是这个过程中会产生一个.pyc文件。在编译阶段,字节码会被写入到__pycache__/的下的一个文件中。不仅仅是自己写的脚本会这样,所有你导入的代码(包括第三方库),都会这样。
所以大多数情况下(除非你写了代码只运行一次?),Python会解释这些字节码,并在本地执行它。我们不妨将其与Java 和 C#.NET进行比较:Java 会被编译为“中间语言”。Java虚拟机读取字节码并将其及时编译为机器码。.NET CIL也是如此。
他们都采用了虚拟机和类似的字节码,为什么Python在基准测试中会比ava 和 C#慢得多呢?首先,Java 和 C#都是及时编译的。
及时编译需要一种中间语言,使得代码能被分为块(或帧)。而提前编译的语言被设计成这样:在任何交互发生前,确保CPU能读懂每段代码。
及时编译本身并不能使运行速度变得更快,因为它执行的仍然是相同的字节码序列。但是即时编译使得运行时的优化成为可能。一个好的即时编译优化器会查看这个应用程序的哪一部分代码经常被执行,并将这部分称为“热点”。然后会对这部分代码做出优化,把他们替换为更有效率的版本。
这意味着当你的应用程序一遍又一遍地重复同样的事情的时候,它的速度将会明显地提升。同时,记住Java 和 C#都是强类型的语言,所以优化器能对代码做出更多的假设。
PyPy是及时编译的,它也比CPython快的多。这篇性能基准测试的文章提供了更多的细节。
2.1 为什么CPython不采用即时编译技术呢?
及时编译有一些缺点,其中一个就是启动时间太慢。CPython的启动时间相对来说已经挺慢的了,PyPy比它还要慢2~3倍。而Java虚拟机启动速度慢更是臭名昭著。
如果你有一个Python程序需要运行很长时间,那么由于“热点”的存在,代码能够被优化。这种情况下,即时编译意义非凡。
但是CPython是多用途的。如果你用Python开发命令行工具,你每在命令行中输入一条命令就要等即时编译器启动,那是相当慢的。
所以如果你想享受及时编译带来的好处,并且有适合的工作,那就用PyPy吧。
3.“因为它是动态类型的语言”
在静态类型的语言中,你在声明一个变量的时候,必须指定它的变量类型。这样的语言包括C, C++, Java, C#, Go。
而在动态类型的语言中,仍然有变量类型的概念,但变量的类型是可以改变的。
静态类型的语言不是为了让你的生活更加艰难才这样设计的,这样设计是因为CPU的工作方式。如果所有东西最终都将等同于简单的二进制操作,你不得不将对象和类型转化为底层的数据结构。
Python也为了做了这样的事,只不过你从没见过,或者你不需要操心这个。
不需要声明类型并不会使Python变慢。Python的语言设计使得你可以将几乎所有的东西都变成动态的。你可以在运行时替换对象中的方法。
正是这样的设计使得优化Python变得异常艰难。
为了阐明我的观点,我使用了一款名为Dtrace的系统调用追踪工具来追踪Python程序的执行过程。有如下发现:比较和转换类型的成本很高, 每次读取、写入或引用变量时, 都需要检查类型。
很难优化如此动态的语言。python 的许多替代方案之所以快得多, 是因为它们为了性能,而对灵活性做出了妥协。
看看cython, 它将 c-静态类型和 python结合了起来, 以优化那些已知变量类型的代码,可以使得性能有84倍的提升。
总结
Python慢的主要原因是它动态的天性和它的多功能性。它可以作为解决各种问题的工具, 但这些问题可能有更优化和更快的解决方案。
但是, 还有一些方法可以优化 python 应用程序,例如:利用异步、理解性能分析工具和使用多解释器。
对于那些启动时间不重要且代码能从即时编译中获利的应用程序, 请考虑使用PyPy。
对于代码中性能非常关键且具有很多静态类型变量的部分, 请考虑使用Cython。