nuxt解决首屏加载慢问题_为什么Python这么慢?为什么需要新的语言解决这个问题?...

我这两天在准备一个Julia的tutorial,面向的观众是一些Python,MATLAB,Fortran的用户,所以准备introduction的时候又需要好好解释这个问题了:为什么我们需要在数值计算(numerical computing)领域(或者说一些其它的领域,比如Go所在的后端)需要一个新语言,Python为什么不能像js一样拥有nodejs?为什么不能直接优化Python代码就好了呢?

我发现在中文互联网上很少有人提这些问题,也没见过有人说过Pyston的这篇文章。这个问题其实Pyston的leader已经详细的在blog里解释过了。所以特此更新一下这个回答。我知道现在很多地方都在各种吹Python,但是在这里泼一下冷水,希望各位在考虑用Python的时候能了解到到底有什么坑,保持一个冷静的态度看待它。

这篇文章其实是我update了这个问题的回答:

python的运行效率在未来是否有可能被优化到跟C++近似?​www.zhihu.com

语言的语义会影响到优化(比如过于动态的Python),pyston已经凉了。现在的大部分Python JIT都不再奢求JIT整个语言了(这包括Torch script,numba等等),甚至PyPy其实都不是Python了(所能够jit的部分),而是另外一门Python-like的语言:RPython。那么为什么Python这么难优化呢?为什么Python就不能用 【这里放上你最喜欢的JIT技术名字】呢?

关于这个问题Pyston项目的leader Kevin Modzelewski曾经有过很详细的思考:优化Python的关键在优化Python的C runtime。下面我翻译一下他的blog post,如果感兴趣也可以读原文,看看我们可以从Pyston的失败中学到什么:

Why is Python slow​blog.kevmod.com

以下是译文


为什么Python这么慢?

以免你错过,Marius最近(注:2016年)写了一篇关于baseline JIT的Pyston的博文。我们的baseline JIT处于解释器层和LLVM JIT层之间,它提供了比解释器更好的速度,但是比LLVM层更快的启动时间(注:JIT启动需要一些时间预热)。

关于这个问题曾经在Hacker News上有一些讨论,而讨论最终转向了一个经常被提到的问题:如果LuaJIT可以拥有一个快速的解释器,为什么我们不能用同样的方式让Python变快呢?这和很多其它类似的问题都是相关的,例如“为什么Python就不能和JavaScript或者Lua一样快呢?”,或者“为什么你就不能在现有的虚拟机上跑Python呢,比如JVM或者CLR“,因为这些问题非常常见,所以我觉得我应该尝试写一篇博文来解释这件事。

问题的源头在:

Python在它的C runtime花了大部分的时间

这意味着其实你执行Python的 “Python”部分有多快并不重要。另外一种解释是说Python的操作码(opcodes)非常复杂,而执行他们的代价使得派发他们的时间微不足道。另一个我经常用的类比是,比起执行JS,执行Python更像是HTML渲染 -- 它更多是一种对运行时(runtime)应该如何执行的描述,而非显式地单步执行命令。

Pyston的性能提升来自于提高这些C代码,而不是Python代码。当人们开始问 “为什么Pyston不能用 【在这里插入你最喜欢的JIT技术】“的时候,我的问题是这项技术可以提高C代码吗?而不是Python代码。这也是为什么我对通过在已有的虚拟机上加速Python代码完全没有兴趣的原因,因为这只会让问题更加复杂。

我认为另外一个事实是考虑到很多人都尝试过提高Python解释器的性能。如果这真的和 “将LuaJIT的技术引入Python” 一样简单,我们早已经完成了。

我最近其实对这件事情做过报告,你可以在这里找到 我的幻灯片 和 一个 LWN文章。在报告里我给出了解释器的overhead实际上很小的一些证据,以及一些C 运行时很慢的例子(比如实际上缓慢的for循环和Python的字节码并没什么关系)。

观众所提的问题里就有 “真的有人觉得Python的性能差是因为解释器吗?“,显然他们没看HackerNews。:)

那么为什么Python的C运行时很慢呢?

这里有一个我在报告里用到的例子。这是一个用Python写出来的for循环,但是它并没有执行任何Python的字节码:

import itertools
sum(itertools.repeat(1.0, 100000000))

而有件很惊人的事情就是如果你在JS里写一个和这个等价的for循环,V8可以让它比CPython快 6倍 。我在报告里错误地将其归结为boxing(注:一种类型数据结构,每个变量里存了值和类型,你可以在CPython的API里看到PyObject的数据结构)的overhead,但是Raymond Hettinger指出这是因为 CPython的 sum() 有特别的优化来避免当输入是浮点数(或者整数)的时候,boxing带来的性能损失。所以这不是boxing带来的性能损失,也不是因为在派发 tp_as_number -> tp_add 时来加和变量。

我当前的最好解释是它只是需要做很多事情,而不是因为C的运行时很慢(尽管种种迹象表明它确实很慢)。在这个 itertools 的例子中,大约 50% 的时间都花在捕获浮点数异常上了。另外 50% 时间被花在找出如何迭代一个 itertools.repeat 对象,然后检查返回的值是否一个float类型。所有这些检查都很快,也被精心优化过,但是它们在每次循环都会发生一遍。一个粗略的估计是CPython每次循环要使用大约30个CPU周期,虽然不是很多,但是也比V8相当多了(V8是5)。

我想我应该试试回答其它在HackerNews上讨论到的点:

如果 JS/Lua 可以很快,为什么Python不能参考它们的方法然后变快呢?

Python是一个非常非常动态的语言,甚至比JS更加动态。完整的解释这一点可能会需要再写一篇很长的博文,但是我可以说从JS到Python所增加的动态性要比从Java到JS都多。我不是很熟悉Lua,但是感觉它比Java或者Python更接近JS。

为什么我们不重写Python的C运行时然后JIT呢?

首先,我们觉得这是一个好主意,因为我认为这个想法在尝试解决导致Python的性能低的根本问题。但是我对它的实现可行性表示怀疑,这也是为什么Pyston选择了另外一个方向来解决这个问题。

如果你打算用另外一个语言重写Python的运行时,我不认为Python是一个非常好的选择。这个语言有太多的warts(注:直译过来是疣)/特性,所以即便你成功去掉了所有的动态overhead,我也不认为你能最终完成这项任务。

此外,还有一个很现实的问题,就是完整的重写CPython是一项很庞大的工程(CPython超过400k loc,大部分是运行时)。然后还有大量的我们需要运行的C扩展模块(注:Python的C extension,虽然是C写的但实际上也会用到大量的Python运行时,所以优化运行时是有必要的),而理想地,我们希望这些代码在某一天也能够加速。这显然和Python社区对C扩展的生态系统的支持相矛盾,而我的观点是这对于Python来说(注:指C扩展)就和语言的语法一样是组成Python的一部分(所以你需要在这个新的解释器/编译器里也支持C扩展,因为它应当被认为是一个Python实现的一部分)。


关于这篇文章Hacker News上也有很多讨论,我选择一些翻译一下,可以帮助你理解这篇博客想要表达的意思。具体的讨论可以通过 这个链接 访问HackerNews

首先有人评论:

这并没有解释Python为什么慢。你只是在解释Python是如何工作的。为什么C的部分会慢?通常C是很快的。只是说Python的操作码很复杂并不能帮助我理解这个结论,因为如果一条复杂的操作码会花很长时间,这通常是因为它确实需要这么长时间。

Java曾经有一个截然相反的问题:它在 “Java字节码” 层面上执行的太多了,比如字符串(string)操作 - 所以他们增加了更多的更复杂的用C/C++写成的操作码来加速,效果显著。

所以你真正需要解释的是为什么Python的效率很差。对简单的操作都会使用臃肿的数据结构和指针跳跃可能是一个很大的原因。我知道Perl曾经有很多高效的内建函数,曾被认为很快(90年代?)

然后是解答(不是作者)

Python是一个非常动态的语言,而这使得为Python提供JIT异常困难。

Python强大的元编程能力使得它几乎不可能让JIT断言某个它正在做的事情是正确的。inline一个简单的函数调用(call)比如 a.x() 就会很难,因为所有a.x里面的东西都可以修改 - 我不是说人们经常这么做,但是实现一个几乎一样的Python类似物完全没有什么用处。

和固定方法调用的PHP相比(除非你使用runkit,而你不应该这么做),a->x() 将总会是一个方法,只要曾经有一个可用的 a->x

这个方法只要被确认了就永远不会改变。

而不像Java,这两个语言都无法知道当某个方法被调用的时候某个 “a” 类型最终会变成什么。

Java其实也不会知道的很清楚,除非调用是通过interface。但是至少引擎(编译器)是知道到目前为止有多少interface的实现被加载。

而对于PHP和Python来说,他们对从哪里寻找那个让我使用 “::x()"的对象完全是未知的。在PHP里,你需要在遇到每个class的时候查找它,而在Python里你需要验证没有人在运行时替换它。

numpy+numba 是一个很好的方式来限制这些导致性能损失的行为。而且我我宁可用bumpy + 一个Python脚本也不想用C + LAPACK。但是如果你有面向对象的结构或者比较一般地来说,多线程 web/rpc 风格的代码,在这些选择并不能为你提供性能提升。


Julia:在设计语言的时候考虑是否对编译器优化友好

如同上面所讨论的,大部分动态语言在设计的时候都没有考虑过语义(semantic)是否对JIT友好,Python就是一个典型的例子。所以根本的矛盾在我们编程语言所具有的语义有问题,Julia的出发点就在限制语言的动态性,同时尽可能保留大部分动态特征。这是否可能呢?

如果你阅读Julia在2012年的论文,其实论文里已经解释的很清楚了。

https://arxiv.org/pdf/1209.5145.pdf​arxiv.org
Design decisions made under the assumption that a language would be implemented
as an interpreter tend to sabotage the ability to generate efficient code. As Henry Baker
observed of Common LISP, “...the polymorphic type complexity of the Common LISP library
functions is mostly gratuitous, and both the efficiency of compiled code and the efficiency of
the programmer could be increased by rationalizing this complexity.” [3] Others have echoed
these sentiments [7] [25].

所以Julia在设计的时候考虑以下三点:

  1. 从语义上为编译器尽可能丰富的类型信息,这导致Julia使用多重派发(实际上torch script为了达到类似的目的也使用了多重派发)
  2. 尽可能的通过运行时类型对代码进行特化(实际上我们在C++里手动对模板这么做,想想你是如何将C++接到Python的C API里的?你有没有和我一样想过:假如填入模板的参数可以在需要调用这个函数的时候从Python传进来呢?)
  3. 通过LLVM来进行JIT

所以为什么有这么多使用JIT的语言,Julia有什么独特的地方?Julia尝试在语义上为LLVM的JIT提供足够丰富的信息。使用LLVM JIT只是因为没必要自己重新写一个JIT的轮子而已。

In past work on optimizing dynamic languages, researchers have observed that programs are
not as dynamic as their authors might think: “We found that dynamic features are pervasive
throughout the benchmarks and the libraries they include, but that most uses of these features
are highly constrained...” [16]. 

而实际上,大部分的程序都不需要Python这么强的动态性,一些动态性其实是完全没必要的,所以我们可以通过限制一些动态性,再增加一些其它特性来使得语言的使用体验和Python几乎一样。Julia假设以下的动态性是有用的:

  • 在加载时期和编译时期运行代码的能力,这可以帮助减少构建系统和配置文件带来的额外负担(比如静态编译语言常常需要配置文件,构建系统比如cmake等)
  • 只需要唯一一个静态类型 Any,这可以使得静态类型在需要的时候可以被忽略
  • 不能拒绝在语法有很好的形式的代码
  • 程序的行为只由运行时类型决定(比如 没有静态重载)

而Julia加入了以下的限制:

  • 类型本身是不可变类型
  • 值的类型无法在生存周期发生改变
  • 局部变量的环境不会具体化(想想解释执行,每个局部都会具体成某个指令)
  • 程序的代码是不可变的(但是可以产生新的代码)
  • 不是所有的binding都是可变的(允许使用常数绑定,注意不是并不是常数)

由于语言的设计编译器可以在类型稳定的时候(type stable)进行静态编译,但是静态编译是发生在一个函数或者代码在运行前的,这个时候调用这段代码的外部变量的类型已经通过运行时进行具体化,我们已经通过运行时获得了更多的类型信息,使得我们能够根据runtime信息将这部分代码specialize(想想我上面提到的在C++模版连接Python的时候,所需的POD类型被自动从运行时计算出来了)这使得即便是非常generic的代码依然能够有效的具体化,但是因为每次dispatch都是根据运行时的类型来进行的,如果实在无法执行依然可以解释执行。所以在这一点上我们还可以像使用Python一样去动态地派发方法。

而类型推导也变得简单起来——因为我们总有足够充分的类型信息,即便有编译时期,编译时期和运行时期是统一的,所有的类型都是运行时的,我们不需要区分到底谁是值谁是类型。

但是我们在上面的文章里提到了Python实际上有很强的元编程能力:它能在运行时期修改自己的代码,但是Julia不行。怎么办呢?Julia引入了Lisp-like的宏来解决这个问题,然后为了让代码不能被修改(因为这对编译器优化不友好),引入了world机制,每段代码的编译都发生在一个world里,旧的world是不能被新的world操作的。

上面的这些要求这使得Julia很自然地拥有了一个具有多级编译(multi staged compilation)的编译器:宏在使得Julia具有强大的元编程能力的同时获得了改变parsing时期的能力。而在0.6加入的generated函数使得Julia获得了在运行时期改变类型推导的能力,在0.7时期加入的SSA Value IR完成了multi stage programming的最后一块拼图:修改JIT行为。

所以在其它Python框架开始造自己的IR(因为Python的语义导致后端能够拿到的信息非常少),并且大量编写编译工具的时候(例如 MLIR,XLA等等)。Julia只要使用自己的多级编译能力就好了。这也就是 Zygote项目 和CUDAnative以及JuliaXLA项目。 可以去GitHub上看看这些项目实际上都非常小1w行以内的量级,因为它们复用了大量编译器功能,而这些功能在Python端往往需用用C++重写一遍。在numba还在为上层信息的缺失想办法的时候,Julia的函数已经可以被transpile到PTX(也就是CUDA)上了,因为代码所具有的信息足够充分。

当然,我不是说所有人都应该用Julia,因为Julia的一些特性是为数值计算/技术计算这个领域定制的。和很多新的语言各有侧重点一样,它是一个侧重数值计算/技术计算的general purpose language。

我是想说:Python的语义上的动态性对于大部分工程实际上是多余的,所以为了加速Python的方案只可能是加速Python的子集,对语义做一些限制。而这也不是长久之计,因为新的工程总是不可避免要写更多的定制数据结构。所以Dropbox放弃了Pyston,选择了Go,一些做高性能计算的开发者放弃了Python + C/C++ 选择了Julia。甚至是PyTorch的作者soumith在Twitter上表示:如果需要定制的类型和数据结构(针对机器学习这个domain来说)他会考虑Julia,例如图卷积。有趣的是:PyTorch刚刚release了一个高性能的Python图卷机库。Ref:

https://twitter.com/soumithchintala/status/1114924295065559040​twitter.com

所以说各个领域都在各显神通,用不同的语言,不同的方式(比如JIT sub-language,或者编写Pythonic c++,比如xtensor这样的项目)去弥补Python + C++带来的问题,原因都是类似的:Python性能不好,整体优化也没有希望,新的project需要很多定制类型,而C++对新人又不友好。

以上,结论就是如果要考虑性能Python整个语言是不太可能再被优化的。我们需要新的语言的语义去平衡新的需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值