Lua 的速度为什么比 Python 快?
程序员文章站
2022-05-21 13:29:11
...
Lua 和 Python 同为虚拟机解释型脚本语言,为什么 Lua 的执行速度比 Python 高?
@冯东 和 @庞巍伟 都提到了Lua使用的是register-based的虚拟机设计,我看到下面有人评论说既然这种VM的设计性能高,那么为什么Python和java还是使用的stack-based的设计。
我的理解是实现难度吧,register-based的设计中,一个操作需要关注到指令的操作数到底存放在哪里,而stack-based的不需要,它分开了几条指令,首先加载数据到栈顶,然后再进行操作,操作时默认的认为数据就存在栈顶了。(如果不清楚这个过程,可以拖上去看看 @庞巍伟 的回答,就不在这里列出来了)
简单的说,register-based的指令格式设计把stack-based的指令中分几条指令要完成的事情用一条指令搞定了,快当然是快了,难度也加大了。
另外还有一点上面的回答中似乎没有提到,Lua使用的是一遍遍历就生产指令的方式,学过编译原理的,大概都能知道一般分两遍遍历,第一遍生成AST,再一遍遍历AST生成指令,而在Lua中是直接跳过了AST指令这一步的。
还是那句话,快是快了,代码的实现难度也大了些。最早的Lua解释器,也是使用lex、yacc这样的工具来自动生成代码的,后来为了提升性能,作者改成了自己手写的递归下降的分析器。这部分代码是我认为Lua代码中最难理解的一个部分了--因为它要一遍分析干太多的事情了。
我在阅读Lua代码的过程中,能充分感受到作者为了Lua在性能上的提升花费的心血,致敬。 有一些 PUC-Rio Lua(也就是没 JIT 的)和 Python 的 benchmark 对比。结论是 Python 比 C 大约慢 70 倍,Lua 大约慢 30-40 倍。
Lua 是 register-based VM。所谓的寄存器,其实并不神秘,就是 runtime stack 的 topmost frame [1] 是可以被 VM 指令随机访问的。至于为什么 CPU 里的某种硬件也叫寄存器,原因在这里有解释:《什么是寄存器》。
Stack frame 可以被随机访问之后,在同一个 VM 指令里就可以用 native code 一次做很多事情。
可以看云风的这篇 blog:《云风的 BLOG: 虚拟机之比较,lua 5 的实现》
最关键的是, lua 是基于寄存器的虚拟机实现,而python还有很多其他脚本语言是基于堆栈的,基于寄存器的虚拟机字节码更简单,更高效,因为register based vm的字节码,一般同时包含了指令/操作数/操作目标等.
对比简单的加法操作:
stack based 生成的字节码大概是这样(仅仅是模拟,不代表实际)
PUSH 1
PUSH 2
ADD // ADD 的操作结果存放eax
PUSH eax // 将结果push入堆栈,以便后面的代码不会覆盖eax
而register based 生成的字节码大概是这样:
ADD 1,2,R1
就一行,R1存放1+2的结果
就这么简答的操作就已经相差4条指令,所以基于寄存器的虚拟机字节码运行更有效率. python的一些设计特性,例如完全面向对象,同时也是它在性能表现上的负担。
举个很简单的例子:
lua 最新的5.3 已经支持整数类型, 加上基于寄存器的VM和优秀的编译器, 想慢都难 抛个砖,引个玉。
1. 基于栈和基于寄存器的不同是主要的性能差异原因。这点大家也都解析的非常清楚了,也很好想象。基于栈的求值过程必须使用栈顶的值,想想也知道是反人类的(哦不,反机器的-_-!)。因此会出现很多的push(load)和pop(store)指令,而基于寄存器的指令就一条完事了。但这都是在解释执行的情况下,如果编译到本地指令之后,理论上来说,基于寄存器还是基于栈的实现并没有太多影响,因为都转换成了硬件寄存器,两者的转换过程的开销也没有太多差别。
2. 为什么采用基于栈的虚拟机,除了实现简单(后序遍历AST就有了)之外,占用空间小也是一个基于栈的虚拟机的特点,便于网络传输和嵌入式设备。Java在设计之初就是考虑到网络方面的应用,比如Applet技术,以及嵌入式设备的运用。
3. 个人还有一个想法,不知道是否靠谱@RednaxelaFX。基于栈的虚拟机的指令更加完整地保留了源代码的求值过程,几乎是AST直接『压平』的结果,甚至很容易逆回源代码。这就意味着基于栈的指令在后续操作中可以很容易转换成需要的形式,以便于在不同的形式上做优化。栈代码转换成寄存器代码没有什么效率影响,而寄存器代码转换成栈代码就会出现比遍历AST生成还要多的push(load)和pop(store)指令。栈代码的生成适合直接从AST后续遍历得到,因为求值的过程都是围绕着栈顶。简而言之,栈代码是一个可塑性比较强的代码,先存着,后面想怎么处理都保留了可能性。
4. 语法分析到代码生成过程减少pass数,个人觉得并没有太大的意义。严格来讲这个过程的效率应该不能算是performance的效率,最多只是加快了从源码的启动时间。Performance应该从解释执行开始比较。有的时候单趟编译造成了复杂性反而得不偿失,AST这样的数据结构就适合在上面干该干的事。
5. 实际中虚拟机的效率还和很多其他啊因素有关。比如很重要的方面就是垃圾回收。
6. 至于上升到指令集设计高度的话,不太了解,请R大来。 @RednaxelaFX。他应该会贴个这个传送门虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 Lua的指令集非常非常非常简单,我对着指令说明看了半个小时就能看懂lua的汇编代码了,再花十来分钟就能手动修改lua二进制代码了。而我甚至没完整看过lua的源码。 占坑 以我的观点,最大的关键是在 lua 在语言层面相比 python 简单了很多,所以他们的实现相应的就有了速度的差别。
回复内容:
前面几位已经说的很好,我来做一下补充。@冯东 和 @庞巍伟 都提到了Lua使用的是register-based的虚拟机设计,我看到下面有人评论说既然这种VM的设计性能高,那么为什么Python和java还是使用的stack-based的设计。
我的理解是实现难度吧,register-based的设计中,一个操作需要关注到指令的操作数到底存放在哪里,而stack-based的不需要,它分开了几条指令,首先加载数据到栈顶,然后再进行操作,操作时默认的认为数据就存在栈顶了。(如果不清楚这个过程,可以拖上去看看 @庞巍伟 的回答,就不在这里列出来了)
简单的说,register-based的指令格式设计把stack-based的指令中分几条指令要完成的事情用一条指令搞定了,快当然是快了,难度也加大了。
另外还有一点上面的回答中似乎没有提到,Lua使用的是一遍遍历就生产指令的方式,学过编译原理的,大概都能知道一般分两遍遍历,第一遍生成AST,再一遍遍历AST生成指令,而在Lua中是直接跳过了AST指令这一步的。
还是那句话,快是快了,代码的实现难度也大了些。最早的Lua解释器,也是使用lex、yacc这样的工具来自动生成代码的,后来为了提升性能,作者改成了自己手写的递归下降的分析器。这部分代码是我认为Lua代码中最难理解的一个部分了--因为它要一遍分析干太多的事情了。
我在阅读Lua代码的过程中,能充分感受到作者为了Lua在性能上的提升花费的心血,致敬。 有一些 PUC-Rio Lua(也就是没 JIT 的)和 Python 的 benchmark 对比。结论是 Python 比 C 大约慢 70 倍,Lua 大约慢 30-40 倍。
Lua 是 register-based VM。所谓的寄存器,其实并不神秘,就是 runtime stack 的 topmost frame [1] 是可以被 VM 指令随机访问的。至于为什么 CPU 里的某种硬件也叫寄存器,原因在这里有解释:《什么是寄存器》。
Stack frame 可以被随机访问之后,在同一个 VM 指令里就可以用 native code 一次做很多事情。
可以看云风的这篇 blog:《云风的 BLOG: 虚拟机之比较,lua 5 的实现》
- Stack frame 就是 stack 中属于同一个 function invocation 的所有 stack entries。
最关键的是, lua 是基于寄存器的虚拟机实现,而python还有很多其他脚本语言是基于堆栈的,基于寄存器的虚拟机字节码更简单,更高效,因为register based vm的字节码,一般同时包含了指令/操作数/操作目标等.
对比简单的加法操作:
stack based 生成的字节码大概是这样(仅仅是模拟,不代表实际)
PUSH 1
PUSH 2
ADD // ADD 的操作结果存放eax
PUSH eax // 将结果push入堆栈,以便后面的代码不会覆盖eax
而register based 生成的字节码大概是这样:
ADD 1,2,R1
就一行,R1存放1+2的结果
就这么简答的操作就已经相差4条指令,所以基于寄存器的虚拟机字节码运行更有效率. python的一些设计特性,例如完全面向对象,同时也是它在性能表现上的负担。
举个很简单的例子:
def test():
a = 1
b = 2
return a + b
Lua的语言特性设计的很紧凑,在各方面进行优化的困难路径都比JS和Python少(得多),
Python基于纯对象的, 任意东西都是对象. 所以数值运算时, 还要进行转换lua 最新的5.3 已经支持整数类型, 加上基于寄存器的VM和优秀的编译器, 想慢都难 抛个砖,引个玉。
1. 基于栈和基于寄存器的不同是主要的性能差异原因。这点大家也都解析的非常清楚了,也很好想象。基于栈的求值过程必须使用栈顶的值,想想也知道是反人类的(哦不,反机器的-_-!)。因此会出现很多的push(load)和pop(store)指令,而基于寄存器的指令就一条完事了。但这都是在解释执行的情况下,如果编译到本地指令之后,理论上来说,基于寄存器还是基于栈的实现并没有太多影响,因为都转换成了硬件寄存器,两者的转换过程的开销也没有太多差别。
2. 为什么采用基于栈的虚拟机,除了实现简单(后序遍历AST就有了)之外,占用空间小也是一个基于栈的虚拟机的特点,便于网络传输和嵌入式设备。Java在设计之初就是考虑到网络方面的应用,比如Applet技术,以及嵌入式设备的运用。
3. 个人还有一个想法,不知道是否靠谱@RednaxelaFX。基于栈的虚拟机的指令更加完整地保留了源代码的求值过程,几乎是AST直接『压平』的结果,甚至很容易逆回源代码。这就意味着基于栈的指令在后续操作中可以很容易转换成需要的形式,以便于在不同的形式上做优化。栈代码转换成寄存器代码没有什么效率影响,而寄存器代码转换成栈代码就会出现比遍历AST生成还要多的push(load)和pop(store)指令。栈代码的生成适合直接从AST后续遍历得到,因为求值的过程都是围绕着栈顶。简而言之,栈代码是一个可塑性比较强的代码,先存着,后面想怎么处理都保留了可能性。
4. 语法分析到代码生成过程减少pass数,个人觉得并没有太大的意义。严格来讲这个过程的效率应该不能算是performance的效率,最多只是加快了从源码的启动时间。Performance应该从解释执行开始比较。有的时候单趟编译造成了复杂性反而得不偿失,AST这样的数据结构就适合在上面干该干的事。
5. 实际中虚拟机的效率还和很多其他啊因素有关。比如很重要的方面就是垃圾回收。
6. 至于上升到指令集设计高度的话,不太了解,请R大来。 @RednaxelaFX。他应该会贴个这个传送门虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 Lua的指令集非常非常非常简单,我对着指令说明看了半个小时就能看懂lua的汇编代码了,再花十来分钟就能手动修改lua二进制代码了。而我甚至没完整看过lua的源码。 占坑 以我的观点,最大的关键是在 lua 在语言层面相比 python 简单了很多,所以他们的实现相应的就有了速度的差别。
声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
相关文章
相关视频