最近在做Python相关的一些东西,发现Python的性能实在是非常差,所以就深入到Python内部,看了一下它的实现,并对比了几个比较流行的虚拟机的实现,包括:
- V8 (Javascript)
- Tamarin (ActionScript 3)
- Lua 5.0
- CPython (Python 2.7.2)
做了一定分析和比对,获得了一些灵感,在这里写下来作一个分享的讨论。
Ok,先从计算机是如何将一个高级语言的代码,转变成可以执行的程序说起。
计算机程序语言的机制
众所周知,计算机能够执行的代码是机器码,也就是所谓的二进制。那么一段高级语言的代码要想能够被计算机执行,必须经过这样的一个过程:
- 编译 (将源代码编译成目标代码:目标代码是机器码片段的集合,每一段机器码都有一个名字,也就是这段代码的符号)
- 连接 (将多个目标文件中的符号连接在一起,形成一个大的可执行机器码,这样计算机(大部分时候是操作系统)就可以加载、执行代码了)
编译器的设计
在早期,编译的过程是直译式的,编译器直接将源代码解析成Token流,再将Token流分析成AST(抽象语法树),然后直接根据抽象语法树中的语法元素生成目标机器的汇编代码,最后再通过汇编器(Assembler)汇编成目标文件。
然而现代的编译器都会有一种中间代码,然后将编译器分成两半(前端和后端)。
前端是语言相关的,负责将原始的语言编译成中间代码;
后端是目标机器相关的,负责将中间代码翻译成目标机器的机器码。
这样做的好处就在于,编译器变得更加可移植了。
当出现一种新的语言时,只要实现一个这个语言的前端,就可以工作在不同的平台和cpu上;
当出现一个新的平台时,只要实现一个后端,就可以支持所有的语言。
GCC就是这样设计的一个范例。
跑题了?
你可能会讲,这不应该是一篇关于虚拟机的文章吗?为何要扯那么多编译的事情呢?
看官勿躁,且听我娓娓道来,这是虚拟机的基础知识:)
我们现在重新整理一下思路,看看这个过程,和每个过程的产物:
阶段 |
产物 |
编码 |
源代码 |
解析 |
AST(抽象语法树) |
前端代码生成 |
中间语言 |
后端代码生成 |
目标文件 |
连接 |
可执行文件 |
虚拟机的实现
从广义上来说,虚拟机的种类繁多,但我们这里特指跨平台的,用于实现语言功能的虚拟机,例如python虚拟机,javascript虚拟机等等。
从上一节的内容,我们可以看出,假设语言都实现到了生成中间语言这一步,那么虚拟机的实现可以有2种方式:
这两种方式各有特色:
我们来横向比较一下这几个虚拟机的实现方式:
虚拟机 |
语言 |
语言特性 |
虚拟机实现方式 |
速度 |
V8 |
JavaScript |
较丰富 |
Binary Translation |
非常快 |
Tamarin |
ActionScript 3(EcmaScript 4) |
较丰富 |
Interpreting + Binary Translation |
非常快 |
Lua 5.0 |
Lua 5.0 |
较少 |
Interpreting |
较快 |
CPython |
Python 2.7 |
很丰富 |
Interpreting |
较慢 |
从这里,我们可以看到使用Jit方式执行的虚拟机明显比较快,而采用解释执行的虚拟机明显较慢。
然而同样是采用解释执行的虚拟机,lua也要比python更快,这不仅仅是由于语言更简单导致的,同时也跟lua虚拟机的实现有关。
V8是一朵奇葩
V8虚拟机可以说是所有的虚拟机里面设计最特别的一款,所有其他的虚拟机都会首先将源代码编译成一种中间代码,如:
虚拟机 |
中间代码 |
指令数 |
Tamarin |
abc (Adobe Byte Code) |
200+ |
Lua |
Lua Byte Code |
35 |
Python |
Python Byte Code |
100+ |
然而,v8虚拟机的方式很特别,它在进行jit的时候直接从ast生成目标平台的汇编代码,并使用内置的宏汇编器生成可执行代码,这样就大大减少了jit过程所消耗的时间。
Lua的指令为什么那么少?
哈哈,细心的读者一定发现了,Lua虚拟机的指令要比其他的虚拟机少很多,甚至不再同一个数量级上,那为什么那么设计呢?
且听下回分解~