JavaScript深入V8引擎以及编写优化代码的5个技巧


Posted in Javascript onJune 24, 2019

概述

JavaScript引擎是执行 JavaScript 代码的程序或解释器。JavaScript引擎可以实现为标准解释器,或者以某种形式将JavaScript编译为字节码的即时编译器。

以为实现JavaScript引擎的流行项目的列表:

  • V8 — 开源,由 Google 开发,用 C ++ 编写
  • Rhino — 由 Mozilla 基金会管理,开源,完全用 Java 开发
  • SpiderMonkey — 是第一个支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用
  • JavaScriptCore — 开源,以Nitro形式销售,由苹果为Safari开发
  • KJS — KDE 的引擎,最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Edge
  • Nashorn, 作为 OpenJDK 的一部分,由 Oracle Java 语言和工具组编写
  • JerryScript —  物联网的轻量级引擎

为什么要创建V8引擎?

由谷歌构建的V8引擎是开源的,使用c++编写。这个引擎是在谷歌Chrome中使用的,但是,与其他引擎不同的是 V8 也用于流行的 node.js。

V8最初被设计用来提高web浏览器中JavaScript执行的性能。为了获得速度,V8 将 JavaScript 代码转换成更高效的机器码,而不是使用解释器。它通过实现 JIT (Just-In-Time) 编译器将 JavaScript 代码编译为执行时的机器码,就像许多现代 JavaScript 引擎(如SpiderMonkey或Rhino (Mozilla)) 所做的那样。这里的主要区别是 V8 不生成字节码或任何中间代码。

V8 曾有两个编译器

在 V8 的 5.9 版本出来之前,V8 引擎使用了两个编译器:

  • full-codegen — 一个简单和非常快的编译器,产生简单和相对较慢的机器码。
  • Crankshaft — 一种更复杂(Just-In-Time)的优化编译器,生成高度优化的代码。

V8 引擎也在内部使用多个线程:

  • 主线程执行你所期望的操作:获取代码、编译代码并执行它
  • 还有一个单独的线程用于编译,因此主线程可以在前者优化代码的同时继续执行
  • 一个 Profiler 线程,它会告诉运行时我们花了很多时间,让 Crankshaft 可以优化它们
  • 一些线程处理垃圾收集器

当第一次执行 JavaScript 代码时,V8 利用 full-codegen 编译器,直接将解析的 JavaScript 翻译成机器代码而不进行任何转换。这使得它可以非常快速地开始执行机器代码。请注意,V8 不使用中间字节码,从而不需要解释器。

当代码已经运行一段时间后,分析线程已经收集了足够的数据来判断应该优化哪个方法。

接下来,Crankshaft  从另一个线程开始优化。它将 JavaScript 抽象语法树转换为被称为 Hydrogen 的高级静态单分配(SSA)表示,并尝试优化 Hydrogen 图,大多数优化都是在这个级别完成的。

内联代码

第一个优化是提前内联尽可能多的代码。内联是用被调用函数的主体替换调用点(调用函数的代码行)的过程。这个简单的步骤允许下面的优化更有意义。

JavaScript深入V8引擎以及编写优化代码的5个技巧

隐藏类

JavaScript是一种基于原型的语言:没有使用克隆过程创建类和对象。JavaScript也是一种动态编程语言,这意味着可以在实例化后轻松地在对象中添加或删除属性。

大多数 JavaScript 解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置,这种结构使得在 JavaScript 中检索属性的值比在 Java 或 C# 等非动态编程语言中的计算成本更高。

在Java中,所有对象属性都是在编译之前由固定对象布局确定的,并且无法在运行时动态添加或删除(当然,C#具有动态类型,这是另一个主题)。

因此,属性值(或指向这些属性的指针)可以作为连续缓冲区存储在存储器中,每个缓冲区之间具有固定偏移量, 可以根据属性类型轻松确定偏移的长度,而在运行时可以更改属性类型的 JavaScript 中这是不可能的。

由于使用字典查找内存中对象属性的位置效率非常低,因此 V8 使用了不同的方法:隐藏类。隐藏类与 Java 等语言中使用的固定对象(类)的工作方式类似,只是它们是在运行时创建的。现在,让我们看看他们实际的例子:

JavaScript深入V8引擎以及编写优化代码的5个技巧

一旦 “new Point(1,2)” 调用发生,V8 将创建一个名为 “C0” 的隐藏类。

JavaScript深入V8引擎以及编写优化代码的5个技巧

尚未为 Point 定义属性,因此“C0”为空。

一旦第一个语句“this.x = x”被执行(在“Point”函数内),V8 将创建一个名为 “C1” 的第二个隐藏类,它基于“C0”。 “C1”描述了可以找到属性 x 的存储器中的位置(相对于对象指针)。

在这种情况下,“x”存储在偏移0处,这意味着当将存储器中的 point 对象视为连续缓冲区时,第一偏移将对应于属性 “x”。 V8 还将使用 “类转换” 更新 “C0” ,该类转换指出如果将属性 “x” 添加到 point 对象,则隐藏类应从 “C0” 切换到 “C1”。 下面的 point 对象的隐藏类现在是“C1”。

JavaScript深入V8引擎以及编写优化代码的5个技巧

每次将新属性添加到对象时,旧的隐藏类都会更新为指向新隐藏类的转换路径。隐藏类转换非常重要,因为它们允许在以相同方式创建的对象之间共享隐藏类。如果两个对象共享一个隐藏类并且同一属性被添加到它们中,则转换将确保两个对象都接收相同的新隐藏类以及随其附带的所有优化代码。

当语句 “this.y = y” 被执行时,会重复同样的过程(在 “Point” 函数内部,“this.x = x”语句之后)。

一个名为“C2”的新隐藏类会被创建,如果将一个属性 “y” 添加到一个 Point 对象(已经包含属性“x”),一个类转换会添加到“C1”,则隐藏类应该更改为“C2”,point 对象的隐藏类更新为“C2”。

JavaScript深入V8引擎以及编写优化代码的5个技巧

隐藏类转换取决于将属性添加到对象的顺序。看看下面的代码片段:

JavaScript深入V8引擎以及编写优化代码的5个技巧

现在,假设对于p1和p2,将使用相同的隐藏类和转换。那么,对于“p1”,首先添加属性“a”,然后添加属性“b”。然而,“p2”首先分配“b”,然后是“a”。因此,由于不同的转换路径,“p1”和“p2”以不同的隐藏类别结束。在这种情况下,以相同的顺序初始化动态属性好得多,以便隐藏的类可以被重用。

内联缓存

V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存依赖于这样一种观察,即对同一方法的重复调用往往发生在同一类型的对象上。这里可以找到对内联缓存的深入解释。

接下来将讨论内联缓存的一般概念(如果您没有时间通过上面的深入了解)。

那么它是如何工作的呢? V8 维护了在最近的方法调用中作为参数传递的对象类型的缓存,并使用这些信息预测将来作为参数传递的对象类型。如果 V8 能够很好地预测传递给方法的对象的类型,它就可以绕过如何访问对象属性的过程,而是使用从以前的查找到对象的隐藏类的存储信息。

那么隐藏类和内联缓存的概念如何相关呢?无论何时在特定对象上调用方法时,V8 引擎都必须执行对该对象的隐藏类的查找,以确定访问特定属性的偏移量。在同一个隐藏类的两次成功的调用之后,V8 省略了隐藏类的查找,并简单地将该属性的偏移量添加到对象指针本身。对于该方法的所有下一次调用,V8 引擎都假定隐藏的类没有更改,并使用从以前的查找存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。

内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。 如果你创建两个相同类型和不同隐藏类的对象(正如我们之前的例子中所做的那样),V8将无法使用内联缓存,因为即使这两个对象属于同一类型,它们对应的隐藏类为其属性分配不同的偏移量。

JavaScript深入V8引擎以及编写优化代码的5个技巧

这两个对象基本相同,但是“a”和“b”属性的创建顺序不同。

编译成机器码

一旦 Hydrogen 图被优化,Crankshaft 将其降低到称为 Lithium 的较低级表示。大部分的 Lithium 实现都是特定于架构的。寄存器分配往往发生在这个级别。

最后,Lithium 被编译成机器码。然后就是 OSR :on-stack replacement(堆栈替换)。在我们开始编译和优化一个明确的长期运行的方法之前,我们可能会运行堆栈替换。 V8 不只是缓慢执行堆栈替换,并再次开始优化。相反,它会转换我们拥有的所有上下文(堆栈,寄存器),以便在执行过程中切换到优化版本上。这是一个非常复杂的任务,考虑到除了其他优化之外,V8 最初还将代码内联。 V8 不是唯一能够做到的引擎。

有一种叫去优化的安全措施来进行相反的转换,并在假设引擎无效的情况下返回未优化的代码。

垃圾收集

对于垃圾收集,V8采用传统的 mark-and-sweep 算法 来清理旧一代。 标记阶段应该停止JavaScript执行。 为了控制GC成本并使执行更稳定,V8使用增量标记:不是遍历整个堆,尝试标记每个可能的对象,它只是遍历堆的一部分,然后恢复正常执行。下一个GC停止将从上一个堆行走停止的位置继续,这允许在正常执行期间非常短暂的暂停,如前所述,扫描阶段由单独的线程处理。

如何编写优化的 JavaScript

  1. 对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏的类和随后优化的代码。
  2. 动态属性: 因为在实例化之后向对象添加属性将强制执行隐藏的类更改,并降低之前隐藏类所优化的所有方法的执行速度,所以在其构造函数中分配所有对象的属性。
  3. 方法:重复执行相同方法的代码将比仅执行一次的多个不同方法(由于内联缓存)的代码运行得更快。
  4. 数组:避免稀疏数组,其中键值不是自增的数字,并没有存储所有元素的稀疏数组是哈希表。这种数组中的元素访问开销较高。另外,尽量避免预分配大数组。最好是按需增长。最后,不要删除数组中的元素,这会使键值变得稀疏。
  5. 标记值:V8 使用 32 位表示对象和数值。由于数值是 31 位的,它使用了一位来区分它是一个对象(flag = 1)还是一个称为 SMI(SMall Integer)整数(flag = 0)。那么,如果一个数值大于 31 位,V8会将该数字装箱,把它变成一个双精度数,并创建一个新的对象来存放该数字。尽可能使用 31 位有符号数字,以避免对 JS 对象的高开销的装箱操作。

Ignition and TurboFan

随着2017年早些时候发布V8 5.9,引入了新的执行管道。 这个新的管道在实际的JavaScript应用程序中实现了更大的性能提升和显着节省内存。

新的执行流程是建立在 Ignition( V8 的解释器)和 TurboFan( V8 的最新优化编译器)之上的。

自从 V8 5.9 版本问世以来,由于 V8 团队一直努力跟上新的 JavaScript 语言特性以及这些特性所需要的优化,V8 团队已经不再使用 full-codegen 和 Crankshaft(自 2010 年以来为 V8 技术所服务)。

这意味着 V8 整体上将有更简单和更易维护的架构。

这些改进只是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
用Jquery实现多级下拉框无刷新的联动
Dec 22 Javascript
window.opener用法和用途实例介绍
Aug 19 Javascript
基于jquery css3实现点击动画弹出表单源码特效
Aug 31 Javascript
JS组件Bootstrap Table使用实例分享
May 30 Javascript
jQuery操作之效果详解
May 19 jQuery
vue中使用element-ui进行表单验证的实例代码
Jun 22 Javascript
js中Object.defineProperty()方法的不详解
Jul 09 Javascript
vue router 源码概览案例分析
Oct 09 Javascript
vue增加强缓存和版本号的实现方法
May 01 Javascript
深入理解 JS 垃圾回收
Jun 03 Javascript
JS数据类型STRING使用实例解析
Dec 18 Javascript
JavaScript仿京东秒杀倒计时
Mar 17 Javascript
JS实现给数组对象排序的方法分析
Jun 24 #Javascript
新手快速入门JavaScript装饰者模式与AOP
Jun 24 #Javascript
Electron + vue 打包桌面操作流程详解
Jun 24 #Javascript
JS字符串常用操作方法实例小结
Jun 24 #Javascript
新手入门带你学习JavaScript引擎运行原理
Jun 24 #Javascript
vue+elementUI 复杂表单的验证、数据提交方案问题
Jun 24 #Javascript
新手如何快速理解js异步编程
Jun 24 #Javascript
You might like
PHP中调用SVN命令更新网站方法
2015/01/07 PHP
Laravel 5 框架入门(三)
2015/04/09 PHP
phpinfo无法显示的原因及解决办法
2019/02/15 PHP
$.ajax json数据传递方法
2008/11/19 Javascript
javascript 面向对象思想 附源码
2009/07/07 Javascript
js对象之JS入门之Array对象操作小结
2011/01/09 Javascript
js/jquery获取浏览器窗口可视区域高度和宽度以及滚动条高度实现代码
2012/12/17 Javascript
jQuery - css() 方法示例详解
2014/01/16 Javascript
修改或扩展jQuery原生方法的代码实例
2015/01/13 Javascript
js+CSS实现弹出居中背景半透明div层的方法
2015/02/26 Javascript
jQuery实现新消息闪烁标题提示的方法
2015/03/11 Javascript
JavaScript实现in-place思想的快速排序方法
2016/08/07 Javascript
Map.vue基于百度地图组件重构笔记分享
2017/04/17 Javascript
JS实现禁止高频率连续点击的方法【基于ES6语法】
2017/04/25 Javascript
浅谈原生JS中的延迟脚本和异步脚本
2017/07/12 Javascript
理解javascript async的用法
2017/08/22 Javascript
使用jQuery 操作table 完成单元格合并的实例
2017/12/27 jQuery
nodejs图片处理工具gm用法小结
2018/12/12 NodeJs
JavaScript 中判断变量是否为数字的示例代码
2020/10/22 Javascript
Python os.rename() 重命名目录和文件的示例
2018/10/25 Python
python判断自身是否正在运行的方法
2019/08/08 Python
python线程的几种创建方式详解
2019/08/29 Python
Python中__repr__和__str__区别详解
2019/11/07 Python
python 爬取百度文库并下载(免费文章限定)
2020/12/04 Python
解析html5 canvas实现背景鼠标连线动态效果代码
2019/06/17 HTML / CSS
会计系毕业个人自荐信格式
2013/09/23 职场文书
计算机软件个人的自荐信范文
2013/12/01 职场文书
中层干部竞聘演讲稿
2014/05/15 职场文书
经济国贸专业求职信
2014/06/18 职场文书
大学生档案自我鉴定(2篇)
2014/10/14 职场文书
2014最新自愿离婚协议书范本
2014/11/19 职场文书
机器人总动员观后感
2015/06/09 职场文书
公司员工离职感言
2015/08/03 职场文书
python 网络编程要点总结
2021/06/18 Python
浅谈MySQL函数
2021/10/05 MySQL
Mysql事务索引知识汇总
2022/03/17 MySQL