paint-brush
“Python 很慢”和垂死时代的其他神话经过@oleksandrkaleniuk
13,386 讀數
13,386 讀數

“Python 很慢”和垂死时代的其他神话

经过 Oleksandr Kaleniuk7m2023/01/11
Read on Terminal Reader

太長; 讀書

编程不像以前那么简单。 Python 是解释型的,速度慢,而 C++ 是编译型的,速度快。一种语言不是编译器或解释器,它就是它本身——一种语言:一组我们应该如何告诉计算机我们应该做什么的规则。
featured image - “Python 很慢”和垂死时代的其他神话
Oleksandr Kaleniuk HackerNoon profile picture


小时候,编程很简单。我的朋友有一台电脑,有 Basic 和 Assembly。你可以用 Basic 编写你的程序,这更容易做,但你的程序会很慢,或者你可以用 Assembly 编写一些东西,这更难,但你的程序运行速度会快得多。


对此的解释也很简单。 Basic 是一个解释器,为了运行你的程序,它必须在你每次调用它时检查你的代码,并逐行解释它。如果它说“PRINT X”,解释器必须找到一个名为“X”的变量,找到一个执行打印的例程,并为找到的变量调用找到的例程。

组装就是组装。它也确实以某种方式解释您的程序,但只有在您运行汇编器时解释一次。之后,您的程序无需解释即可执行。需要解释的程序比不需要解释的程序运行得慢。当然,如果它们是等价的程序。

在 Basic 和 Assembly 中,它们通常是。 Basic是一种命令式语言,甚至对结构化编程都不太友好。即使像“函数”这样重要的东西也不是内置的语言结构,而是一种模式:“GOSUB ... RETURN”,非常类似于 Assembly 中的“call ... ret”。

现在快进 30 年。语言很丰富。计算机无处不在。编程不再简单了。我的部门通过用 C++ 重写研究人员最初用 Python 编写的代码来提高性能,因为常识是 Python 是解释型的,速度慢,而 C++ 是编译型的,速度快。但不知何故,每年这种重写变得越来越难赢得任何性能。有些东西在变化,而且变化很快。然而,常识并没有改变,所以我们不断重写。

但现在我们被迫疯狂地优化一切,只是为了证明我们所做的事情是合理的。 Python 进来一个算法,我们用 C++ 等价重写,突然慢了 3 倍。那是……不是我们来这里的目的。因此,我们重新设计了算法以推动并获得我们承诺的性能提升。大多数时候,由于研究人员根本不关心性能和算法方面的问题,他们确实会留下一些唾手可得的成果,所以这是可行的。

尽管如此,这整个业务现在看起来像一个骗局。我们通过用 C++ 重写代码来降低代码速度,这样我们就可以通过重新设计代码来提高速度。那我们为什么不直接用 Python 重新设计呢?啊!问题是,我们不了解 Python。我们知道一点 Python,足以阅读和理解,但不足以在其中制作超快的程序。

那有什么要知道的呢?

图书馆

大多数 Python 库都是用 C 或 Fortran 编写的。 NumPy核心是用 C 语言编写的;熊猫- 在 Cython 和 C 中; SciPy - 在 Fortran、C 和部分 C++ 中。它们没有理由比用 C++、Rust 或 Julia 编写的任何东西都慢。不过它们可以更快。

在我们公司,我们同时迎合云服务和桌面应用程序。当桌面用户最喜欢的应用程序的新版本显然无缘无故地停止在他们的硬件上工作时,他们会很生气。所以我们保持我们的桌面构建目标是旧的。真的很老,就像 Nehalem 之前的老。这样,没有人会生气,但也没有人会享受 SSE3。


当然,为适当目标构建的计算库通常比为具有有限超标量功能的 15 岁通用计算机构建的等效库更快。

好消息是,如果你正在为云构建,你可以将你的目标构建平台设置为你购买的机器,然后你的 C++ 库将以与 Python 库相同的速度运行,甚至可能更快一点。

编译器

老实说,关于哪种语言更快的整个争论都是荒谬的。一种语言不是编译器或解释器,它就是它本身——一种语言:一组规则,指定我们应该如何告诉计算机我们想要做什么。语言只是一组规则,一种规范。没有别的。


解释和汇编之间的区别是上个世纪的事情。如今,有IGCCPicoCCCons等 C 解释器,也有 Python 编译器。 JIT 编译器,例如 [PyPy] 和经典的先编译后运行编译器,例如Codon (如果您只想编译部分代码,它也具有 JIT 功能)。


Codon 建立在 LLVM 之上,与 Rust、Julia 或 Clang 建立在相同的基础设施之上。使用 Codon 构建的代码运行、给予或获取的性能水平与使用其中任何一个构建的相同。由于 Python 的垃圾收集或大型本机数据类型,可能存在性能劣势,但我们不再谈论 100 倍或 10 倍。 LLVM 发挥它的魔力。它为您将 Python 代码转换为机器代码。


还有关于即时编译或 JIT 的神话。有人说它优于通常的先编译后运行技术,因为它始终针对用户拥有的体系结构进行编译,从而最佳地利用它。有人说仍然存在编译开销,即时编译也会落在用户身上。这使得程序运行缓慢,因为它们必须同时运行和编译自己。


这两个神话的问题在于它们都是真实的,但都没有帮助。是的,JIT 通常会编译成更好的机器代码,除非您明确地为目标机器构建二进制文件,顺便说一句,当您在云中部署时,这种情况经常发生。是的,在运行时会有编译损失,但如果您的运行时间以月为单位,则可以忽略不计,同样,当您在云中部署时,这并不是闻所未闻的事情。


所以有利有弊。重要的是,Python(特别是 Codon)支持先编译后运行和 JIT 模式,因此您可以选择最适合您需要的模式。 Clang 等传统编译器没有 JIT 选项。

Numba 及其内核模型

说到 JIT, Numba可能是超快速 Python 编程领域中最具颠覆性的技术。它是一个编译器,但它只针对选定的内核,而不是整个程序。当然,您可以选择应该编译什么以及针对哪个平台。在此设置中,您可以在 CPU 上运行您的代码片段,在GPGPU上运行其他代码。


从技术上讲,可以为其他专用设备创建后端,例如 Google 的TPU甚至 Lightmatters 的光子加速器。目前还没有这样的后端,那些人决定推出他们自己的库。但是,有症状的是,他们还选择在 Python 中为光子计算机提供接口,以便您可以与 Pytorch、Tensorflow 或 ONNX 无缝交互。


所以 Lightmatter 还不存在。但英伟达是。他们确实为 Numba 提供了他们的 CUDA 后端,你现在可以用 Python 编写内核,并以最高效率在 NVidia 硬件上运行它们。 C++没有那个。然而,来自NVidia的 CU 方言当然在这方面扩展了 C++。在 Python 中,您不必扩展语言本身。由于 Numba 用作 JIT 内核编译器,因此添加后端只是给库打补丁的问题。


因此,内核模型针对的是异构计算。您可以在不同的设备上运行代码片段,这本身就很好。但是您可能没有考虑到异质性的另一个方面。使用内核模型,您可以针对不同的计算上下文而不一定是硬件设备使用不同的内核。这意味着,如果您希望一个内核速度快但不是特别精确,您可以使用“-fast-math”选项构建它。但是,如果在某些其他情况下,您希望该内核精确而不是快速,您可以重建完全相同的代码而无需权衡。


这是传统编译器难以实现的,因为传统编译器无法在翻译单元中间更改编译选项。好吧,对于内核模型,每个内核都是它自己的翻译单元。

结论

Python 并不慢。它也不快。它只是一种语言,一组规则和关键字。但是有很多人已经习惯了这些规则和这些关键词。他们觉得用 Python 写作很舒服,他们有兴趣让 Python 变得更好。


这个用户群足够大,既可以吸引拥有革命性技术(如 Lightmatter 及其光子计算机)的新初创公司,也可以吸引在高性能计算领域拥有数十年专业知识的知名公司(如 NVidia)。所有这些人都投入巨资让 Python 成为更好的……当然不是语言,而是环境。编写超快程序的环境比编写慢速的一次性程序要困难一些。


总之,他们正在取得巨大进步。 Python 每年都在变得越来越快。在这一点上,如果可以的话,请忘掉光子计算机,用 Python 编写的程序通常可以与用 Julia、C++ 或 Rust 编写的程序运行。但 Python 不会就此止步。它比传统编译器变得更快,并且对用户更友好。


准备好在几年内看到 Python 编译器:PyPy、Numba 或一些全新的东西,将借鉴SpiralHerbie的技术来更有效地生成代码,以至于任何传统编译器都无法与之匹敌。毕竟,用 Python 编写新的 JIT 后端比重新构想整个 LLVM 基础设施要容易得多。