为什么Python这么慢?

  • 2019 年 11 月 25 日
  • 笔记

Python越来越受欢迎。它被用于DevOps、数据科学、Web开发和安全。

然而,它并没有赢得任何速度奖牌。

就速度而言,Java与C或c++或c#或Python相比如何?

答案在很大程度上取决于您正在运行的应用程序的类型。没有一个基准测试是完美的,但是计算机语言基准测试游戏是一个很好的起点。

十多年来,我一直在参考计算机语言基准测试游戏;与其他语言如Java、c#、Go、JavaScript、c++相比,Python是最慢的语言之一。这包括JIT (c#, Java)和AOT (C, c++)编译器,以及解释语言,如JavaScript。

注:当我说“Python”时,我指的是该语言的参考实现CPython。Python是一门语言,有语法等规范。但是落实到具体实现上,就不一样了。用C实现的叫CPython,也是目前的参考实现。即最新的语言特性都是在这个上面先实现,Linux,OS X等自带的也是这个版本。用.NET实现的叫IronPython,Java的叫Jython,用Python实现的叫PyPy

我想回答这个问题:当Python比另一种语言慢2 – 10倍完成一个可比较的应用程序时,为什么它慢,我们不能使它更快?

以下是最热门的理论:

  • 它是GIL(全局解释器锁)"
  • 因为它是解释过的而不是编译过的
  • 因为它是动态类型语言

这些原因中哪一个对性能影响最大?

我们逐个分析

1. 它是GIL(全局解释器锁)

现代计算机的CPU是多核的,有时也有多个处理器。为了利用所有这些额外的处理能力,操作系统定义了一个称为线程的底层结构,其中一个进程(如Chrome浏览器)可以衍生多个线程,并在内部为系统提供指令。通过这种方式,如果一个进程是cpu密集型的,那么可以跨内核共享负载,从而有效地使大多数应用程序更快地完成任务。

如果您以前没有做过多线程编程,那么您需要快速熟悉锁的概念。与单线程进程不同,您需要确保在更改内存中的变量时,多个线程不会尝试同时访问/更改相同的内存地址。

当CPython创建变量时,它分配内存,然后计算有多少对该变量的引用存在,这是一个称为引用计数的概念。如果引用的数量为0,那么它将从系统中释放那块内存。这就是为什么在for循环的范围内创建“临时”变量不会增加应用程序的内存消耗。

当变量在多个线程中共享时,挑战就变成了CPython如何锁定引用计数。有一个“全局解释器锁”,它小心地控制线程的执行。解释器一次只能执行一个操作,不管它有多少线程

那么其他Python runtimes呢?

PyPy有一个GIL,它通常比CPython快3倍。

Jython没有GIL,因为Jython中的Python线程由Java线程表示,并且受益于JVM内存管理系统。

JavaScript是如何做到这一点的?

首先,所有Javascript引擎都使用标记-清除垃圾收集。如前所述,GIL的主要需求是CPython的内存管理算法。

JavaScript没有GIL,但它也是单线程的,所以不需要GIL。JavaScript的事件循环和承诺/回调模式是实现异步编程而不是并发的方式。Python对异步事件循环也有类似的处理。

2. 因为这是一种解释语言

我经常听到这种说法,我发现这是对CPython实际工作方式的一种粗略简化。如果您在终端上编写了python myscript.py,那么CPython将开始一长串的读取、词法分析、解析、编译、解释和执行这些代码

在这个过程中很重要的一点是创建一个.pyc文件,在编译器阶段,字节码序列被写到Python 3上的_pycache__/中的一个文件中,或者在Python 2的相同目录中。这不仅适用于您的脚本,还适用于您导入的所有代码,包括第三方模块。

所以大多数时候(除非您编写的代码只运行一次?),Python都是解释字节码并在本地执行它。与Java和c# .NET相比: Java编译成“中间语言”,Java虚拟机读取字节码并及时将其编译成机器码。net CIL是一样的,. net公共语言运行时(CLR)对机器代码使用即时编译。

那么,如果Python都使用虚拟机和某种字节码,那么为什么在基准测试中它比Java和c#慢那么多呢?

首先,. net和Java是jit编译的。JIT或即时编译需要一种中间语言来允许将代码分割成块(或帧)。提前(AOT)编译器的设计是为了确保CPU在进行任何交互之前能够理解代码中的每一行。

JIT本身并没有使执行变得更快,因为它仍然在执行相同的字节码序列。但是,JIT允许在运行时进行优化。一个好的JIT优化器会看到应用程序的哪些部分被频繁地执行,称之为“热点”。然后,它将对这些代码进行优化,用更高效的版本替换它们。

这意味着当您的应用程序一次又一次地做同样的事情时,它可以显著地更快。另外,请记住Java和c#是强类型语言,因此优化器可以对代码进行更多的假设。

PyPy有一个JIT,正如前一节所提到的,它比CPython要快得多。

那么为什么CPython不使用JIT呢?

jit也有缺点:其中之一就是启动时间。CPython的启动时间已经比较慢了,PyPy比CPython慢2 – 3倍。众所周知,Java虚拟机的启动速度很慢。net CLR通过在系统启动时启动来解决这个问题,但是CLR的开发人员还开发运行CLR的操作系统。

如果您有一个运行了很长时间的Python进程,其中的代码可以进行优化,因为它包含“热点”,那么JIT就很有意义。

然而,CPython是一种通用实现。因此,如果您正在使用Python开发命令行应用程序,那么每次调用CLI时都必须等待JIT启动,这将是非常慢的。

CPython必须尝试并服务尽可能多的用例。在CPython中插入JIT是有可能的,但是这个项目在很大程度上已经停止了。如果您希望获得JIT的好处,并且有适合它的工作负载,那么可以使用PyPy。

3. 因为它是动态类型语言

在“静态类型”语言中,必须在声明变量时指定变量的类型。包括C, c++, Java, c#, Go。在动态类型语言中,仍然有类型的概念,但是变量的类型是动态的。

a = 1  a = "foo"

在这个例子中,Python创建了第二个具有相同名称和str类型的变量,并释放为a的第一个实例创建的内存

静态类型语言的设计并不是为了让您的工作变得困难,而是因为CPU的操作方式。如果最终需要将所有操作都等同于简单的二进制操作,则必须将对象和类型转换为低级数据结构。

Python为您做了这些,您只是从来没有见过它,也不需要关心它。

不需要声明类型并不是使Python变慢的原因,Python语言的设计使您能够使几乎任何东西都是动态的。您可以在运行时替换对象上的方法,您可以在运行时对低级系统调用的值进行monkey-patch。几乎一切皆有可能。

正是这种设计使得优化Python变得非常困难

那么,Python的动态类型会使它变慢吗?

  • 比较和转换类型的成本很高,每次读取、写入或引用某个变量时,都要检查该类型
  • 很难优化一门如此动态的语言。Python的许多替代品之所以如此之快,是因为它们在性能的名义下对灵活性做出了妥协
  • 看看Cython,它结合了C-Static类型和Python来优化已知类型的代码,可以提供84x的性能改进。

结论

Python的主要缺点是它的动态性和通用性。它可以作为解决各种问题的工具,在这些问题中,可能有更优化、更快的替代方案。

但是,可以通过利用异步、理解分析工具和考虑使用多解释器来优化Python应用程序。

对于启动时间不重要且代码有利于JIT的应用程序,可以考虑使用PyPy。

对于您的代码中性能非常重要并且有更多静态类型变量的部分,可以考虑使用Cython。

原文传送门:

https://medium.com/hackernoon/why-is-python-so-slow-e5074b6fe55b