为什么 Python 这么慢?

Python 正在蓬勃发展。它用于 DevOps、数据科学、Web 开发和安全。

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

AI吧Python

Java 在速度方面与 C 或 C++ 或 C# 或 Python 相比如何?答案很大程度上取决于您正在运行的应用程序的类型。没有基准是完美的,但计算机语言基准游戏是一个很好的起点

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

注意:当我说“Python”时,我指的是语言的参考实现,CPython。我将在本文中提及其他运行时。

我想回答这个问题:当 Python 完成一个类似的应用程序比另一种语言慢 2-10 倍时,为什么它很慢,我们不能让它更快

以下是顶级理论:

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

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

“这是GIL”

现代计算机配备有多个内核的 CPU,有时还有多个处理器。为了利用所有这些额外的处理能力,操作系统定义了一个称为线程的低级结构,其中一个进程(例如 Chrome 浏览器)可以生成多个线程并在内部拥有系统指令。这样,如果一个进程特别占用 CPU 资源,则该负载可以跨内核共享,这有效地使大多数应用程序更快地完成任务。

在我撰写本文时,我的 Chrome 浏览器打开了44 个线程。请记住,线程的结构和 API 在基于 POSIX(例如 Mac OS 和 Linux)和 Windows OS 之间是不同的。操作系统还处理线程的调度。

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

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

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

这对 Python 应用程序的性能意味着什么?

如果您有一个单线程、单解释器应用程序。这对速度没有影响。删除 GIL 不会影响代码的性能。

如果您想通过使用线程在单个解释器(Python 进程)中实现并发,并且您的线程是 IO 密集型的(例如网络 IO 或磁盘 IO),您会看到 GIL 争用的后果。

来自 David Beazley 的 GIL 可视化帖子http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html

如果你有一个 web 应用程序(例如 Django)并且你正在使用 WSGI,那么对你的 web 应用程序的每个请求都是一个单独的 Python 解释器,所以每个请求只有一个锁。因为 Python 解释器启动缓慢,一些 WSGI 实现有一个“守护程序模式” ,它可以让 Python 进程为您运行。

其他 Python 运行时呢?

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

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

JavaScript 如何做到这一点?

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

JavaScript 没有 GIL,但它也是单线程的因此不需要 GIL。JavaScript 的事件循环和 Promise/Callback 模式是实现异步编程代替并发的方式。Python 与 asyncio 事件循环有相似之处。

“这是因为它是一种解释性语言”

我经常听到这种说法,我发现这是对 CPython 实际工作方式的大体简化。如果在您编写的终端上,python myscript.pyCPython 将开始读取、词法分析、解析、编译、解释和执行该代码的长序列。

如果您对该过程的工作方式感兴趣,我之前已经写过:6分钟修改Python语言本周我向 CPython 核心项目提出了我的第一个拉取请求,该请求被拒绝了 🙁 但至于不完全……黑客中午网

该过程中的一个重点是创建.pyc文件,在编译器阶段,字节码序列被写入__pycache__/Python 3 内部的文件或 Python 2 的同一目录中。这不仅适用于您的脚本,而且您导入的所有代码,包括第 3 方模块。

所以大多数时候(除非你编写只运行一次的代码?),Python 是在解释字节码并在本地执行它。将其与 Java 和 C#.NET 进行比较:

Java 编译为“中间语言”,Java 虚拟机读取字节码并即时将其编译为机器码。.NET CIL 也是如此,.NET Common-Language-Runtime,CLR,使用即时编译来机器代码。

那么,如果 Python 都使用虚拟机和某种字节码,为什么在基准测试中 Python 比 Java 和 C# 慢得多?首先,.NET 和 Java 是 JIT 编译的。

JIT 或即时编译需要一种中间语言来允许将代码拆分为块(或帧)。提前 (AOT) 编译器旨在确保 CPU 在任何交互发生之前能够理解代码中的每一行。

JIT 本身不会使执行速度更快,因为它仍在执行相同的字节码序列。但是,JIT 可以在运行时进行优化。一个好的 JIT 优化器会看到应用程序的哪些部分被执行了很多,称之为“热点”。然后,它将通过用更高效的版本替换这些代码来优化这些代码。

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

PyPy 有一个 JIT,如上一节所述,它比 CPython 快得多。这篇性能基准文章更详细地介绍了——哪个是 Python 最快的版本?当然,“这取决于”,但它取决于什么以及如何评估哪个是 Python 的最快版本……黑客中午网

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

JIT 有缺点:其中之一是启动时间。CPython 的启动时间已经比较慢,PyPy 的启动速度比 CPython 慢 2-3 倍。Java 虚拟机的启动速度是出了名的慢。.NET CLR 通过从系统启动开始解决这个问题,但 CLR 的开发人员也开发了运行 CLR 的操作系统。

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

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

CPython 必须尝试服务尽可能多的用例。有可能将 JIT 插入 CPython,但该项目已基本停滞。

如果您想要 JIT 的好处并且您有适合它的工作负载,请使用 PyPy。

这是因为它是一种动态类型的语言

在“静态类型”语言中,您必须在声明变量时指定变量的类型。这些将包括 C、C++、Java、C#、Go。

在动态类型语言中,还有类型的概念,但是变量的类型是动态的。

a = 1 
a = "foo"

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

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

Python 会为你做这件事,你只是永远看不到它,也不需要关心。

不必声明类型并不是使 Python 变慢的原因,Python 语言的设计使您几乎可以使任何东西动态化。您可以在运行时替换对象上的方法,您可以将低级系统调用猴子修补到运行时声明的值。几乎一切皆有可能。

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

为了说明我的观点,我将使用一个在 Mac OS 中工作的系统调用跟踪工具,称为 Dtrace。CPython 发行版没有内置 DTrace,因此您必须重新编译 CPython。我在演示中使用 3.6.6

wget https://github.com/python/cpython/archive/v3.6.6.zip
解压 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 的许多替代品速度如此之快的原因是它们以性能为名牺牲了灵活性
  • 看看Cython,它结合了 C 静态类型和 Python 来优化已知类型的代码,可以提供84 倍的性能提升。

结论

Python 之所以慢主要是因为它的动态特性和多功能性。它可以用作解决各种问题的工具,其中可能有更优化和更快的替代方案。

但是,有一些方法可以通过利用异步、了解分析工具并考虑使用多解释器来优化 Python 应用程序。

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

C对于性能至关重要并且您有更多静态类型变量的代码部分,请考虑使用Cython. .

进一步阅读

Jake VDP 的优秀文章(虽然有些过时)https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

Dave Beazley 关于 GIL 的演讲http://www.dabeaz.com/python/GIL.pdf

关于 JIT 编译器的所有信息https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/

发表评论

邮箱地址不会被公开。 必填项已用*标注