图解googlev8#13:字节码:v8为什么又重新引入字节码?(代码片段)

凯小默 凯小默     2022-12-05     602

关键词:

说明

图解 Google V8 学习笔记

什么是字节码?

字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象。它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是因为通常每个 opcode 是一字节长,但是指令码的长度是变化的。每个指令有从 0 到 255(或十六进制的: 00 到FF)的一字节操作码,被参数例如寄存器或内存地址跟随。

所谓字节码,是指编译过程中的中间代码。

字节码有两个作用:

  1. 解释器可以直接解释执行字节码 ;
  2. 优化编译器可以将字节码编译为二进制代码,然后再执行二进制机器代码。

早期 V8 执行流水线:直接将 JavaScript 代码编译成机器代码。

  • 基线编译器,它负责将 JavaScript 代码编译为没有优化过的机器代码。
  • 优化编译器,它负责将一些热点代码(执行频繁的代码)优化为执行效率更高的机器代码。

  1. 首先,V8 会将一段 JavaScript 代码转换为抽象语法树 (AST)。
  2. 接下来基线编译器会将抽象语法树编译为未优化过的机器代码,然后 V8 直接执行这些未优化过的机器代码。
  3. 在执行未优化的二进制代码过程中,如果 V8 检测到某段代码重复执行的概率过高,那么 V8 会将该段代码标记为 HOT,标记为 HOT 的代码会被优化编译器优化成执行效率高的二进制代码,然后就执行该段优化过的二进制代码。
  4. 不过如果优化过的二进制代码并不能满足当前代码的执行,这也就意味着优化失败,V8 则会执行反优化操作。

为什么现在使用了字节码 + 解释器 + 编译器方式,抛弃了直接将 JavaScript 代码编译为二进制代码的方式,尽管机器代码的执行性能非常高效,出于什么原因考虑?

  • 时间问题:编译时间过久,影响代码启动速度;
  • 空间问题:缓存编译后的二进制代码占用更多的内存。

机器代码缓存

通过把二进制代码保存在内存中来消除冗余的编译,重用它们完成后续的调用,这样就省去了再次编译的时间。实践表明,在浏览器中采用了二进制代码缓存的方式,初始加载时分析和编译的时间缩短了 20%~40%。

V8 使用了两种代码缓存策略:

  1. 内存缓存(in-memory cache):V8 第一次执行一段代码时,会编译源 JavaScript 代码,并将编译后的二进制代码缓存在内存中。
  2. 硬盘缓存:即便关闭了浏览器,下次重新打开浏览器再次执行相同代码时,也可以直接重复使用编译好的二进制代码。


在早期,Chrome 用了这两种代码缓存的策略来提升 JavaScript 代码的执行速度,以牺牲存储空间来换取执行速度。

JavaScript 代码和二进制代码:


二进制代码所占用的内存空间是 JavaScript 代码的几千倍,V8 过度占用内存,会导致 Web 应用的速度大大降低。

为了解决缓存的二进制机器代码占用过多内存的问题,早期的 Chrome 并没有缓存函数内部的二进制代码,只是缓存了顶层次的二进制代码,采用了惰性编译,其实惰性编译除了能提升 JavaScript 启动速度,还可以解决部分内存占用的问题。

缺陷:如果浏览器只缓存顶层代码,那么闭包模块中的代码将无法被缓存,而对于高度工程化的模块来说,这种模块式的处理方式到处都是,这就导致了一些关键代码没有办法被缓存。

因此,V8 团队对早期的 V8 架构进行了非常大的重构,具体地讲,抛弃之前的基线编译器和优化编译器,引入了字节码、解释器和新的优化编译器。

字节码降低了内存占用

为什么通过引入字节码就能降低 V8 在执行时的内存占用呢?

字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码占用的空间远小于二进制代码,所以浏览器就可以实现缓存所有的字节码,而不是仅仅缓存顶层的字节码。

虽然采用字节码在执行速度上稍慢于机器代码,但是整体上权衡利弊,采用字节码也许是最优解。因为采用字节码除了降低内存之外,还提升了代码的启动速度,并降低了代码的复杂度,而牺牲的仅仅是一点执行效率。

字节码如何提升代码启动速度?

启动 JavaScript 代码的流程图:

  • 生成机器代码比生成字节码需要花费更久的时间
  • 直接执行机器代码却比解释执行字节码要更高效

V8 在使用的模型:

  • V8 的解释器叫 Ignition,(就原始字节码执行速度而言)是所有引擎中最快的解释器。
  • V8 的优化编译器名为 TurboFan,最终由它生成高度优化的机器码。

字节码如何降低代码的复杂度?

早期的 V8 代码,无论是基线编译器还是优化编译器,它们都是基于 AST 抽象语法树来将代码转换为机器码的,不同架构的处理器非常之多

这意味着基线编译器和优化编译器要针对不同的体系的 CPU 编写不同的代码,这会大大增加代码量。

引入了字节码,就可以统一将字节码转换为不同平台的二进制代码:

因为字节码的执行过程和 CPU 执行二进制代码的过程类似,相似的执行流程,那么将字节码转换为不同架构的二进制代码的工作量也会大大降低,这就降低了转换底层代码的工作量。字节码是平台无关的,机器码针对不同的平台都是不一样的。

总的来说字节码的优势有如下三点:

  • 解决启动问题:生成字节码的时间很短;
  • 解决空间问题:字节码占用内存不多,缓存字节码会大大降低内存的使用;
  • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。

参考资料

图解googlev8#08:类型转换:v8是怎么实现1+“2”的?(代码片段)

...明图解GoogleV8学习笔记。什么是类型系统(TypeSystem)?为什么数字1加上字符串2输出的结果是字符串12?要搞清上面这个问题,需要知道类型的概念,以及JavaScript操作类型的策略。对机器语言来说,所有的数据都... 查看详情

图解googlev8#17:消息队列:v8是怎么实现回调函数的?(代码片段)

说明图解GoogleV8学习笔记什么是回调函数?只有当某个函数被作为参数,传递给另外一个函数,或者传递给宿主环境,然后该函数在函数内部或者在宿主环境中被调用,才称为回调函数。回调函数有两种不同的... 查看详情

图解googlev8#16:v8是怎么通过内联缓存来提升函数执行效率的?(代码片段)

说明图解GoogleV8学习笔记什么是内联缓存?内联缓存(Inlinecaching)是部分编程语言的运行时系统采用的优化技术,最早为Smalltalk开发。内联缓存的目标是通过记住以前直接在调用点上方法查询的结果来加快运行时... 查看详情

图解googlev8,搞懂javascript执行逻辑

V8是Google推出的开源高性能JavaScript与WebAssembly引擎,主要的应用包括Chrome浏览器以及Node.js。得益于Chrome浏览器的市场占有率,V8已经成为了当今最主流的JavaScript引擎。 很多前端开发人员对V8的理解还停留在表面,只是单纯地... 查看详情

图解googlev8#01:v8是如何执行一段javascript代码的?

说明图解GoogleV8学习笔记JavaScript的设计思想JavaScript借鉴了很多语言的特性:C语言的基本语法Java的类型系统和内存管理Scheme的函数作为一等公民(为了实现函数是一等公民的特性,JavaScript采取了基于对象的策略)... 查看详情

图解googlev8#18:异步编程:v8是如何实现微任务的?(代码片段)

说明图解GoogleV8学习笔记宏任务和微任务宏任务指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时&... 查看详情

图解googlev8学习笔记合集23篇(完结)

说明这些文章只是笔者学习【图解GoogleV8】专栏记录的笔记,仅供参考。请支持正版的课程。目录图解GoogleV8#01:V8是如何执行一段JavaScript代码的?图解GoogleV8#02:函数即对象:一篇文章彻底搞懂JavaScript的函数... 查看详情

图解googlev8#09:运行时环境:运行javascript代码的基石(代码片段)

说明图解GoogleV8学习笔记运行时环境在执行JavaScript代码之前,V8就已经准备好了代码的运行时环境,包括:堆空间和栈空间全局执行上下文全局作用域内置的内建函数宿主环境提供的扩展函数和对象消息循环系统什么... 查看详情

图解googlev8#20:垃圾回收:v8的两个垃圾回收器是如何工作的?(代码片段)

说明图解GoogleV8学习笔记垃圾数据是怎么产生的?例子:window.test=newObject()window.test.a=newUint16Array(100)上面代码内存布局图:在上面的基础上,添加代码执行:window.test.a=newObject()此时的内存布局:a属... 查看详情

字节码(代码片段)

...译器,TurboFan将字节码生成优化的机器代码。如果想知道为什么会有两种执行模式,可以从JSConfEU查看我(原作者)的视频(YouTube需fq上网):FranziskaHinkelmann:JavaScriptengines-howdotheyeven?|JSConfEU2017字节码是机器代码的抽象。如果字节... 查看详情

图解googlev8#11:堆和栈:函数调用是如何影响到内存布局的?(代码片段)

说明图解GoogleV8学习笔记在编译流水线中的位置先看三个例子:1、在同一个任务中重复调用嵌套的kaimo函数。functionkaimo() kaimo()kaimo()V8会报栈溢出的错误:2、使用setTimeout让kaimo函数在不同的任务中执行。functionkaimo()console.l... 查看详情

什么是googlev8javascript引擎

V8是一个由丹麦Google开发的开源JavaScript引擎,用於GoogleChrome中。[2]LarsBak是这个项目的组长。[3]V8在执行之前将JavaScript编译成了机器码,而非位元组码或是直译它,以此提升效能。更进一步,使用了如内联缓存(inlinecaching)等方... 查看详情

v8字节码的编译过程(代码片段)

v8字节码的编译过程前面的文章中我们学习了调用V8API的方法。本文我们讲解一下v8编译成字节码的主要过程。我们来看一张编译的全局地图:API调用部分我们知道,v8中编译代码的方法是v8::Script::Compile:v8::Local<v8::Script&g... 查看详情

v8字节码的编译过程(代码片段)

v8字节码的编译过程前面的文章中我们学习了调用V8API的方法。本文我们讲解一下v8编译成字节码的主要过程。我们来看一张编译的全局地图:API调用部分我们知道,v8中编译代码的方法是v8::Script::Compile:v8::Local<v8::Script&g... 查看详情

v8字节码的编译过程(代码片段)

v8字节码的编译过程前面的文章中我们学习了调用V8API的方法。本文我们讲解一下v8编译成字节码的主要过程。我们来看一张编译的全局地图:API调用部分我们知道,v8中编译代码的方法是v8::Script::Compile:v8::Local<v8::Script&g... 查看详情

javascript的字节码-v8ignition指令(代码片段)

JavaScript的字节码-v8Ignition指令前面的文章我们介绍了在js的AST层次的各种操作手段。AST操练熟练了之后,就差一步就可以执行了,那就是转换成中间代码,或者是解释型的字节码,或者是为编译器准备的IR.我们以v8... 查看详情

javascript的字节码-v8ignition指令(代码片段)

JavaScript的字节码-v8Ignition指令前面的文章我们介绍了在js的AST层次的各种操作手段。AST操练熟练了之后,就差一步就可以执行了,那就是转换成中间代码,或者是解释型的字节码,或者是为编译器准备的IR.我们以v8... 查看详情

javascript的字节码-v8ignition指令(代码片段)

JavaScript的字节码-v8Ignition指令前面的文章我们介绍了在js的AST层次的各种操作手段。AST操练熟练了之后,就差一步就可以执行了,那就是转换成中间代码,或者是解释型的字节码,或者是为编译器准备的IR.我们以v8... 查看详情