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 相关文章推荐
js里取容器大小、定位、距离等属性搜集整理
Aug 19 Javascript
javascript中处理时间戳为日期格式的方法
Jan 02 Javascript
Jquery焦点与失去焦点示例应用
Jun 10 Javascript
TypeScript 中接口详解
Jun 19 Javascript
深入分析Javascript事件代理
Jan 30 Javascript
通过BootStrap-select插件 js jQuery控制select属性变化
Jan 03 Javascript
详解Angular5 服务端渲染实战
Jan 04 Javascript
JS同步、异步、延迟加载的方法
May 05 Javascript
解决Layui中layer报错的问题
Sep 03 Javascript
layui实现二维码弹窗、并下载到本地的方法
Sep 25 Javascript
element form 校验数组每一项实例代码
Oct 10 Javascript
Vue 图片压缩并上传至服务器功能
Jan 15 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五种设计模式小结
2011/03/23 PHP
thinkphp在模型中自动完成session赋值示例代码
2014/09/09 PHP
PHP加密解密类实例代码
2016/07/20 PHP
详解ThinkPHP3.2.3验证码显示、刷新、校验
2016/12/29 PHP
PHP配合fiddler抓包抓取微信指数小程序数据的实现方法分析
2020/01/02 PHP
mouse_on_title.js
2006/08/25 Javascript
动态样式类封装JS代码
2009/09/02 Javascript
来自qq的javascript面试题
2010/07/24 Javascript
基于jquery的分页控件(C#)
2011/01/06 Javascript
在javascript中随机数 math random如何生成指定范围数值的随机数
2015/10/21 Javascript
分享JavaScript监听全部Ajax请求事件的方法
2016/08/28 Javascript
JavaScript生成.xls文件的代码
2016/12/22 Javascript
js数组去重的hash方法
2016/12/22 Javascript
bootstrap confirmation按钮提示组件使用详解
2017/08/22 Javascript
基于jQuery实现图片推拉门动画效果的两种方法
2017/08/26 jQuery
layui点击按钮添加可编辑的一行方法
2018/08/15 Javascript
详解js实时获取并显示当前时间的方法
2019/05/10 Javascript
React精髓!一篇全概括小结(急速)
2019/05/23 Javascript
js实现移动端tab切换时下划线滑动效果
2019/09/08 Javascript
Vue根据条件添加click事件的方式
2019/11/09 Javascript
vant实现购物车功能
2020/06/29 Javascript
利用Python演示数型数据结构的教程
2015/04/03 Python
Python使用smtplib模块发送电子邮件的流程详解
2016/06/27 Python
Python3 单行多行万能正则匹配方法
2019/01/07 Python
uniapp+Html5端实现PC端适配
2020/07/15 HTML / CSS
美国护肤咨询及美容产品电商:Askderm
2017/02/24 全球购物
利物浦足球俱乐部官方网上商店:Liverpool FC Official Store
2018/01/13 全球购物
日本酒店、民宿、温泉旅馆、当地旅行团中文预订:e路东瀛
2019/12/09 全球购物
在校硕士自我鉴定
2014/01/23 职场文书
房地产财务管理制度
2014/02/02 职场文书
五一劳动节活动记录
2014/03/23 职场文书
商场主管竞聘书
2014/03/31 职场文书
协议书的格式
2014/04/23 职场文书
公司年终奖分配方案
2014/06/16 职场文书
使用Vue3+Vant组件实现App搜索历史记录功能(示例代码)
2021/06/09 Vue.js
纯CSS3实现div按照顺序出入效果
2021/07/15 HTML / CSS