认识 V8 引擎(1)

V8 是一个由 Google 开发的,开源的,高性能的, JavaScript 及 WebAssembly 引擎,使用 C++ 编写。广泛应用于 Google Chrome 浏览器、Node.js 等等。跨平台支持 Windows(Win7以上)、macOS(10.12+),以及各种 Linux 系统,包括 ARM 的手机系统。同时,V8 也可以独立的运行使用,嵌入集成到 C++ 的应用中。

JavaScript 引擎的主要作用是执行 JavaScript 代码。世界上第一款 JavaScript 引擎是 SpiderMonkey,由布兰登·艾克在网景公司开发,用于 Netscape Navigator 网页浏览器。传闻艾克在 1995年 5 月仅花了十天就把原型设计了出来。

早期的 JavaScript 引擎是通过解释器的方式解释执行,比如将源码转成抽象语法树(AST),然后解释执行,其运行效率并不高。但由于早期的浏览器网页还是以静态网页(HTML)为主,JavaScript 并未被广泛重度的应用,所以解释执行并未遇到太大问题。后来随着动态网页兴起,网页里开始运行越来越多复杂的应用,JavaScript 的瓶颈凸显了出来。

既然 JavaScript 运行性能不行,为什么不替换它呢?换成 Lua,Python 不香吗?或者再重新精心设计另外一门专用语言(WebAssembly)。其实替换 JavaScript 的尝试一直都在进行,但是JavaScript 在浏览器里的地位至今无人能撼动。主要原因我认为有三个:

  1. 经过几十年的应用,JavaScript 已经成为事实标准,改变不易

  2. JavaScript 对开发者非常友好,上手较其他语言更容易

  3. 随着 JIT 技术的引入,JavaScript 的性能已经不再是问题

前两个都是习惯问题,即使改变,慢慢也就适应了。但 JIT 技术的引入,几乎解决了 JavaScript 的性能问题,使得 JavaScript 的地位更加的牢固。在 JavaScript 中应用 JIT 的典型代表,就是我们今天的主角:V8。在引入我们的主题之前,我还要简要的介绍一下什么是 JIT 技术。

JIT 的全程是 just-in-time compilation,又译即时编译或实时编译。在 JIT 出现之前,高级编程语言要被计算机执行,通常通过两种方式来实现:AOT 和解释器。AOT 全程 ahead-of-time compilation,即代码预先通过编译器的编译,直接生成目标机器的机器码。因此它的运行效率最高,比如我们常用的 C/C++ 代码,通过 gcc/clang/msvc 等编译器,直接编译出机器码的二进制文件,其运行性能我们称之为原生性能。

既然 AOT 性能最高,全都用 AOT 不就得了?也不尽然。AOT 要求编译的代码是静态类型的,比如一个变量的类型在运行期间不允许变成另一种类型,也不允许动态的往类型里增加属性。而 JavaScript 是动态类型的,比如下面一段代码,变量 foo 的类型可以运行时随时变化:

var foo = 1; // foo is a intfoo = "hello"; // foo change to a stringfunction Point() {

}
foo = new Point(); // foo change to a Pointfoo.x = 10;  // foo add x propertyfoo.y = 10;  // foo add y property

foo 类型总是变来变去,给 AOT 编译器增加了很大的麻烦。同时,AOT 还有编译时间长,生成的目标文件大的问题。它不能像 Java 一样『一次编译,到处运行』,而是针对不同平台『编译多次,到处运行』。如果想像 JavaScript 一样做到『一次编写,到处运行』,就需要在用户的浏览器端,运行时实时的进行编译。这样一来,缓慢的编译过程又拖慢了代码的启动速度。

JIT 结合了 AOT 和解释器两者的优势。它可以运行时根据情况在两者之间切换。比如首次启动时,使用解释器来执行,这样保证了代码的启动速度,对于一些短小的一次性执行的代码非常友好。当代码运行一段时间,编译器发现某些代码频繁的反复执行,则切换到 JIT 模式即时的将这部分代码编译成机器码,之后再运行到这类代码时,则可以做到几乎以原生的速度执行。

现代语言的虚拟机几乎都拥有了 JIT 的能力。如 JVM JIT,LuaJIT。几大 JavaScript 引擎,例如微软的 Chakra、Mozilla 的 SpiderMonkey、苹果的 JavaScriptCore、谷歌的 V8,JIT 已经成了标配功能。而其中谷歌的 V8,是所有的 JavaScript 引擎的佼佼者。

为什么 V8 能做到这么优秀呢?我们后面慢慢来讲。我们先来看看 V8 是怎样诞生出来的。从维基百科里了解到,V8 的创建者是一名叫 Lars Bak 的丹麦程序员。我们来看看这位大神的履历:

  1. 1991 年在 Sun 工作,开发 Self 虚拟机,成为业界最佳程序员之一

  2. 1994 年,离开 Sun,加入 LongView,设计和开发了高性能虚拟机 StrongTalk

  3. 1997 年,LongView 被 Sun 收购,主导开发了著名的 Java 虚拟机 HotSpot

  4. 2002 年回到丹麦,创立名叫 OOVM 的公司。

    2004年将公司卖给一家瑞士公司 Esmertec,随后又在该公司干了两年

  5. 2006 年加入谷歌,在丹麦自己的农场开始开发 V8 引擎

  6. 2008 年,开发的 V8 引擎和谷歌浏览器 Chrome 一起横空出世。

    V8 处理 JavaScript 的速度比当时的 IE 浏览器快 56 倍

  7. 2011 年主导开发并发布 Dart 语言。

    如今应用火热的 Flutter 正是使用该语言进行开发

据说 2006 年是劈柴(Sundar Pichai,现谷歌 CEO)哥亲自给 Bak 打的电话,说谷歌正打算开发一款全新的浏览器,你来做高级经理,开发一个高性能的 JavaScript 引擎好不好?Bak 对开发 JavaScript 引擎很有兴趣,欣然接受了这份工作。但他说他不在乎当什么高级经理,在乎的是推动打破技术的边界,并且他不会回到加州,而是在丹麦自己的农场开始他的工作。他家农场距离加州总部相隔 8000 公里,相差了 9 个时区。

他在农场建立了办公室,家就在办公室对面。每天,他走过石子路到办公室,然后开始写代码。结束工作后,又穿过院子走回家,把工作彻底放下。他享受这种工作和生活分开的感觉。这也是他不想去硅谷的原因。他招募了自己的学生卡斯帕伦德一起来农场工作,命名了新的引擎名字叫 V8,以汽车 V 型 8 缸发动机命名,预示着这将是一款性能爆表的引擎。V8 从零开始开发的,一面世就秒杀了市面上所有的 JavaScript 引擎。

早期的 V8 版本,为了追求性能的极致,将源码转成抽象语法树之后,直接通过 Full-codegen 生成目标机器码。2010 年,又推出了 Crankshaft 编译优化器,在代码执行过程中,内置的 Profiler 记录热点函数,然后提交给 Crankshaft 进行优化,生成优化后的机器码,进一步提高执行的效率。

由于直接生成目标机器码,导致了占用内存大,编译时间长导致启动速度慢,2016 年 V8 设计了中间字节码 Ignition,以让 V8 能在内存更小的安卓设备上流畅的运行。2017 年推出 V8 5.9 版本,废弃了旧的 Full-codegen+Crankshaft 的编译架构,使用了 Ignition 字节码解释器和编译优化器 TurboFan,内存使用得到进一步降低,网页加载速度也得到大幅的提升。Ignition+TurboFan 的组合沿用至今。

2017 年 V8 5.7 版本,正式支持了 WebAssembly,一个基于浏览器设计的新的字节码标准。2018 年 V8 6.9 版本,引入 WebAssembly 基线编译器 Liftoff,极大提升了首次编译的效率,配合 TurboFan 编译优化器,在启动速度和运行性能上都得到了很大的提升。

截止目前,V8 最新的版本是 8.7,并且一直在不断的迭代进化之中。

随想

Lars Bak 从大学时才接触计算机,一直专注在虚拟机的领域,做出像 JVM HotSpot、V8、Dart 这样非凡的产品。这告诉我们,找准自己的兴趣和努力的方向,深耕下去,什么时候开始都不会迟。Bak 加入谷歌时,已经 41 岁。虽然之前他已经积累了不少的财富,但我相信他是由衷的不在乎谷歌的什么高级职位,在乎的只是用更好的技术,突破更多的技术边界。所以 V8 从一开始就是开源的,正如题图 2008 年 Chrome 发布时的宣传漫画里说的一样:『所以,其他浏览器也可以用它(V8),或者,如果有其他项目需要用到 JavaScript,开发者都可以直接使用 V8』

我喜欢写代码,但有时也因程序员 35 岁问题而困扰,了解了 Lars Bak 经历之后,我想,专研深耕自己的领域,专注到兴趣的事情来上,竞争力自然就加强了,也就真没有什么好焦虑了。