How JavaScript works - 2 (译)js 工作原理

1,215 阅读5分钟

inside the V8 engine + 5 tips on how to write optimized code

原文链接 : https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e

Foreword 前言

两周前我们开始了js深入了解以及它如何的工作原理的研究:我们认为通过了解js的代码构建和他们的运行机制可以写出更好的代码和app。
前面的博文主要是js引擎概述,运行和堆栈。第二篇会深入谷歌V8的js引擎部分,我们也会提供一点点关于如何编写更好的js代码的建议-最好

Overview 概览

js引擎是一个程序或者执行js代码解析器。js引擎可以实现为一个标准的翻译,或者以某种形式将js代码即时编译成字节码。 下面是流行的js执行引擎清单列表:

  • v8 - 开源,由google开发,用C++ 编写
  • Rhino — 由火狐管理,开源,完全用java开发
  • SpiderMonkey
  • JavaScriptCore
  • KJS
  • Chakra
  • Chakra
  • Nashorn
  • JerryScript

为何创造v8引擎

由google开发V8引擎是开源的,用c++编写。这个引擎用于Google Chrome。不像其他的引擎,当前流行的node.js也是基于V8运行。

v8最初旨在增加js在web浏览器中的性能。为了获取速度,V8将js代码转换成更高校的机器代码而不是翻译js。他将js代码编译成机器代码,在执行的时候是即时编译的,就像大多数js引擎模型一样。主要的区别在于v8不会产生任何字节码或者任何中间代码。

v8有两个编译器

在V8的5.9版本出现之前,v8使用两个编译器:

  • full-codegen -- 一个简单快速的编译器,生成编译简单和相对慢的机器码。
  • Crankshaft -- 一个更复杂的(即时)优化编译器,生成高度优化的代码。

v8引擎内部也是使用多线程的:

  • 主线程做你想要的工作:获取代码,编译代码然后执行。
  • 还有一个单独的线程编译,所以主线成可以继续执行代码,而前者是优化代码
  • 在运行时有一个分析器的线程会告诉我们哪些方法花费了大量的事件,这样Crankshaf可以去优化它
  • 一些线程用来处理垃圾回收扫描

当开始执行js代码,v8使用full-codegen直接解析js翻译成机器码,没有任何转换。这允许它开始执行机器代码非常快。注意v8不使用中间字节码表示这种方式不需要翻译。

当你的代码运行一段时间后,解析器线程会聚集足够的数据告诉我们哪些方法必须优化。 接下来,Crankshaft开始在另一条线程上优化。它将js抽象语法树(syntax tree )转换成一个叫Hydrogen 的高级静态单元分配表示 (SSA) ,而且尝试去优化 Hydrogen这个图。大多数的优化是在这级完成的。

Inlining 代码嵌入

首次优化尽可能多的提前嵌入代码。代码嵌入是将使用函数的地方(调用函数的那一行)替换成被调用函数的本体。这个简单的步骤可以使接下来的优化更有意义。

Hidden class 隐藏类

js是一个基于原型链的语言:它没有类和对象是通过克隆产生的。js也是一个动态编程语言,这意味着在对象实力化以后可以很简单增加或者移除属性。 大多数js解析器使用类似字典一样的结构(基于散列函数) 去储存对象属性值在内存中的地址。这种构造使js在检索对象属性值上比其他类似java,c++等动态语言,花费更大的计算量。在java中,所有的对象属性在编译之前都已经被固定的对象容器决定。而且在运行时不可以动态的添加或者删除属性。(c++的动态类型是另一个话题)。结果,每一个属性的值能够以连续的buffer储存在内存中,在每个属性值之间有一个偏移量。这个偏移量的长度很容易根据属性类型决定。然而在js中这是不可能的,因为js的属性值可以在运行时改变。 因为使用字典去查找对象属性在内存中的地址的效率很低,V8使用了一个不同的方法代替:隐藏类。隐藏类与在java等语言中使用的固定对象布局的工作方式类似,除了他在运行时被创建。来让我们看一下真实的代码:

 function Point(x,y){
     this.x = x;
     this.y = y;
 }
 var p1 = new Point(1,2);

一旦 new Point(1,2) 被调用,V8会创建一个名为 C0 的隐藏类。

此时他还没有被定义任何属性,因此 C0 是空的。 一旦第一排代码被执行 “this.x = x” (在Point函数中),V8会创建一个基于 C0 名为 C1 的第二个隐藏类。 C1 记录的是属性x在内存中的地址(相对于对象指针)。在这个案例中,x 被存储在 offset偏移量为0中,这意味着当浏览一个作为持续的buffer指针对象的时候,第一个偏移量对应属性 ‘x’。如果属性x被放入制定对象的时候,V8也会用一个过度的类更新C0的状态。隐藏类就会从C0C1。现在指定对象的隐藏类指定C1

每一次新的属性被添加到对象中,旧的隐藏类就会用一个过渡的路径更新到新的隐藏类上。隐藏类的过渡很重要,因为他们允许隐藏类之间共享相同的方式创建对象。如果两个对象分享一个隐藏类,相同的属性会被添加到这两个对象中。过渡可以确保这两个对象接收到相同的隐藏类和所有的优化代码。

Inline caching 内部缓存

Compilation to machine code 编译机械代码

Ignition and TurboFan

How to write optimized JavaScript 如何优化js代码

Resources