JavaScript 引擎 V8 执行流程概述

本文首发于 vivo互联网技术 微信公众号?
链接:https://mp.weixin.qq.com/s/t__Jqzg1rbTlsCHXKMwh6A
作者:赖勇高

本文主要讲解的是V8的技术,是V8的入门篇,主要目的是了解V8的内部机制,希望对前端,快应用,浏览器,以及nodejs同学有些帮助。这里不涉及到如何编写优秀的前端,只是对JS内部引擎技术的讲解。

一、V8来源

V8的名字来源于汽车的“V型8缸发动机”(V8发动机)。V8发动机主要是美国发展起来,因为马力十足而广为人知。V8引擎的命名是Google向用户展示它是一款强力并且高速的JavaScript引擎。

V8未诞生之前,早期主流的JavaScript引擎是JavaScriptCore引擎。JavaScriptCore是主要服务于Webkit浏览器内核,他们都是由苹果公司开发并开源出来。据说Google是不满意JavaScriptCore和Webkit的开发速度和运行速度,Google另起炉灶开发全新的JavaScript引擎和浏览器内核引擎,所以诞生了V8和Chromium两大引擎,到现在已经是最受欢迎的浏览器相关软件。

二、V8的服务对象

V8是依托Chrome发展起来的,后面确不局限于浏览器内核。发展至今V8应用于很多场景,例如流行的nodejs,weex,快应用,早期的RN。

三、V8的早期架构

V8引擎的诞生带着使命而来,就是要在速度和内存回收上进行革命的。JavaScriptCore的架构是采用生成字节码的方式,然后执行字节码。Google觉得JavaScriptCore这套架构不行,生成字节码会浪费时间,不如直接生成机器码快。所以V8在前期的架构设计上是非常激进的,采用了直接编译成机器码的方式。后期的实践证明Google的这套架构速度是有改善,但是同时也造成了内存消耗问题。可以看下V8的初期流程图:

早期的V8有Full-Codegen和Crankshaft两个编译器。V8 首先用 Full-Codegen把所有的代码都编译一次,生成对应的机器码。JS在执行的过程中,V8内置的Profiler筛选出热点函数并且记录参数的反馈类型,然后交给 Crankshaft 来进行优化。所以Full-Codegen本质上是生成的是未优化的机器码,而Crankshaft生成的是优化过的机器码。

四、V8早期架构的缺陷

随着版本的引进,网页的复杂化,V8也渐渐的暴露出了自己架构上的缺陷:

  1. Full-Codegen编译直接生成机器码,导致内存占用大
  2. Full-Codegen编译直接生成机器码,导致编译时间长,导致启动速度慢
  3. Crankshaft 无法优化try,catch和finally等关键字划分的代码块
  4. Crankshaft新加语法支持,需要为此编写适配不同的Cpu架构代码

五、V8的现有架构

为了解决上述缺点,V8采用JavaScriptCore的架构,生成字节码。这里是不是感觉Google又绕回来了。V8采用生成字节码的方式,整体流程如下图:

Ignition是V8的解释器,背后的原始动机是减少移动设备上的内存消耗。在Ignition之前,V8的Full-codegen基线编译器生成的代码通常占据Chrome整体JavaScript堆的近三分之一。这为Web应用程序的实际数据留下了更少的空间。

Ignition的字节码可以直接用TurboFan生成优化的机器代码,而不必像Crankshaft那样从源代码重新编译。Ignition的字节码在V8中提供了更清晰且更不容易出错的基线执行模型,简化了去优化机制,这是V8 自适应优化的关键特性。最后,由于生成字节码比生成Full-codegen的基线编译代码更快,因此激活Ignition通常会改善脚本启动时间,从而改善网页加载。

TurboFan是V8的优化编译器,TurboFan项目最初于2013年底启动,旨在解决Crankshaft的缺点。Crankshaft只能优化JavaScript语言的子集。例如,它不是设计用于使用结构化异常处理优化JavaScript代码,即由JavaScript的try,catch和finally关键字划分的代码块。很难在Crankshaft中添加对新语言功能的支持,因为这些功能几乎总是需要为九个支持的平台编写特定于体系结构的代码。

采用新架构后的优势

不同架构下V8的内存对比,如图:

结论:可以明显看出Ignition+TurboFan架构比Full-codegen+Crankshaft架构内存降低一半多。

不同架构网页速度提升对比,如图:

结论:可以明显看出Ignition+TurboFan架构比Full-codegen+Crankshaft架构70%网页速度是有提升的。

接下来我们大致的讲解下现有架构的每个流程:

六、V8的词法分析和语法分析

学过编译原理的同学可以知道,JS文件只是一个源码,机器是无法执行的,词法分析就是把源码的字符串分割出来,生成一系列的token,如下图可知不同的字符串对应不同的token类型。

词法分析完后,接下来的阶段就是进行语法分析。语法分析语法分析的输入就是词法分析的输出,输出是AST抽象语法树。当程序出现语法错误的时候,V8在语法分析阶段抛出异常。

七、V8 AST抽象语法树

下图是一个add函数的抽象语法树数据结构

V8 Parse阶段后,接下来就是根据抽象语法树生成字节码。如下图可以看出add函数生成对应的字节码:

BytecodeGenerator类的作用是根据抽象语法树生成对应的字节码,不同的node会对应一个字节码生成函数,函数开头为Visit****。如下图+号对应的函数字节码生成:

void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* expr) {
  FeedbackSlot slot = feedback_spec()->AddBinaryOpICSlot();
  Expression* subexpr;
  Smi* literal;

  if (expr->IsSmiLiteralOperation(&subexpr, &literal)) {
    VisitForAccumulatorValue(subexpr);
    builder()->SetExpressionPosition(expr);
    builder()->BinaryOperationSmiLiteral(expr->op(), literal,
                                         feedback_index(slot));
  } else {
    Register lhs = VisitForRegisterValue(expr->left());
    VisitForAccumulatorValue(expr->right());
    builder()->SetExpressionPosition(expr);  //  保存源码位置 用于调试
    builder()->BinaryOperation(expr->op(), lhs, feedback_index(slot)); //  生成Add字节码
  }
}

上述可知有个源码位置记录,然后下图可知源码和字节码位置的对应关系:

生成字节码,那字节码如何执行的呢?接下来讲解下:

八、字节码

首先说下V8字节码:

  1. ?每个字节码指定其输入和输出作为寄存器操作数
  2. ?Ignition 使用registers寄存器 r0,r1,r2... 和累加器寄存器(accumulator register)
  3. ?registers寄存器:函数参数和局部变量保存在用户可见的寄存器中
  4. 累加器:是非用户可见寄存器,用于保存中间结果

如下图ADD字节码:

字节码执行

下面一系列图表示每个字节码执行时,对应寄存器和累加器的变化,add函数传入10,20的参数,最终累加器返回的结果是50。

每个字节码对应一个处理函数,字节码处理程序保存的地址在dispatch_table_中。执行字节码时会调用到对应的字节码处理程序进行执行。Interpreter类成员dispatch_table_保存了每个字节码的处理程序地址。

例如ADD字节码对应的处理函数是(当执行ADD字节码时候,会调用InterpreterBinaryOpAssembler类):

IGNITION_HANDLER(Add, InterpreterBinaryOpAssembler) {
   BinaryOpWithFeedback(&BinaryOpAssembler::Generate_AddWithFeedback);
}

void BinaryOpWithFeedback(BinaryOpGenerator generator) {
    Node* reg_index = BytecodeOperandReg(0);
    Node* lhs = LoadRegister(reg_index);
    Node* rhs = GetAccumulator();
    Node* context = GetContext();
    Node* slot_index = BytecodeOperandIdx(1);
    Node* feedback_vector = LoadFeedbackVector();
    BinaryOpAssembler binop_asm(state());
    Node* result = (binop_asm.*generator)(context, lhs, rhs, slot_index,
feedback_vector, false);
    SetAccumulator(result);  // 将ADD计算的结果设置到累加器中
    Dispatch(); // 处理下一条字节码

}

其实到此JS代码就已经执行完成了。在执行过程中,发现有热点函数,V8会启用Turbofan进行优化编译,直接生成机器码。所以接下来讲解下Turbofan优化编译器:

九、Turbofan

Turbofan是根据字节码和热点函数反馈类型生成优化后的机器码,Turbofan很多优化过程,基本和编译原理的后端优化差不多,采用的sea-of-node。

add函数优化:

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);

V8是有函数可以直接调用指定优化哪个函数,执行%OptimizeFunctionOnNextCall主动调用Turbofan优化add函数,根据上次调用的参数反馈优化add函数,很明显这次的反馈是整型数,所以turbofan会根据参数是整型数进行优化直接生成机器码,下次函数调用直接调用优化好的机器码。(注意执行V8需要加上 --allow-natives-syntax,OptimizeFunctionOnNextCall为内置函数,只有加上 --allow-natives-syntax,JS才能调用内置函数 ,否则执行会报错)。

JS的add函数生成对应的机器码如下:

这里会涉及small interger小整数概念,可以查看这篇文章https://zhuanlan.zhihu.com/p/82854566

如果把add函数的传入参数改成字符

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);

优化后的add函数生成对应的机器码如下:

对比上面两图,add函数传入不同的参数,经过优化生成不同的机器码。

如果传入的是整型,则本质上是直接调用add汇编指令

如果传入的是字符串,则本质上是调用V8的内置Add函数

到此V8的整体执行流程就结束了。文章中可能存在理解不正确的地方敬请指出。

  • 参考文章
  1. https://v8.dev/docs
  2. https://docs.google.com/presentation/d/1HgDDXBYqCJNasBKBDf9szap1j4q4wnSHhOYpaNy5mHU/edit#slide=id.g17d335048f\_1\_1105
  3. https://docs.google.com/presentation/d/1Z9iIHojKDrXvZ27gRX51UxHD-bKf1QcPzSijntpMJBM/edit#slide=id.p
  4. https://zhuanlan.zhihu.com/p/82854566

原文地址:https://blog.51cto.com/14291117/2458836

时间: 2024-12-12 20:20:52

JavaScript 引擎 V8 执行流程概述的相关文章

十分钟理解JavaScript引擎的执行机制

关注专栏写文章 十分钟理解JavaScript引擎的执行机制 方伟景 千锋前端开发推动市场提升的学习研究者. 4 人赞同了该文章 首先,请牢记2点: JS是单线程语言 JS的Event Loop是JS的执行机制.深入了解JS的执行,就等于深入了解JS里的event loop 1.灵魂三问:JS为什么是单线程的?为什么需要异步?单线程又是如何实现异步的呢? 技术的出现,都跟现实世界里的应用场景密切相关的.同样的,我们就结合现实场景,来回答这三个问题. (1) JS为什么是单线程的? JS最初被设计

JavaScript 引擎「V8」发布 8.0 版本,内存占用量大幅下降

上周,JavaScript 引擎「V8」的开发团队在该项目官方网站上正式宣布推出最新的 8.0 版本.这次更新的重点主要集中在错误修复及性能改善上,正式的版本将在数周后随着谷歌 Chrome 80 稳定版一起发布. V8 是谷歌公司推出的开源高性能 JavaScript 引擎,主要用于提升 Web 浏览器内部 JavaScript 脚本执行的性能.V8 通过 C++ 语言编写,主要用在 Chrome 浏览器以及 Node.js 上,实现了对 ECMAScript 与 WebAssembly 的支

Javascript引擎单线程机制及setTimeout执行原理说明

setTimeout用法在实际项目中还是会时常遇到.比如浏览器会聪明的等到一个函数堆栈结束后才改变DOM,如果再这个函数堆栈中把页面背景先从白色设为红色,再设回白色,那么浏览器会认为DOM没有发生任何改变而忽略这两句话,因此我们可以通过setTimeout把“设回白色”函数加入下一个堆栈,那么就可以确保背景颜色发生过改变了(虽然速度很快可能无法被察觉). 总之,setTimeout增加了Javascript函数调用的灵活性,为函数执行顺序的调度提供极大便利. 然后,我们从基础的层面来看看:理解J

V8 JavaScript引擎研究(二)高性能探秘

V8的高性能探秘 V8项目负责人Lars Bak:V8的所有优化都不是原创的.V8组合了过往对于动态语言的各种优化技术,因而具有了非常高效的性能. 快速属性访问 JavaScript是动态编程语言,这意味着可以动态的增加或删除对象的属性. 以往实现 大多数的JavaScript引擎都是使用类似字典的数据结构来保存一个对象的属性,在这种结构下每次访问一个属性都需要动态查找其在内存中的位置. 优势: 实现简单. 劣势: 效率低.相对于Java.Smalltalk等语言,由于对象的class有着固定的

V8 JavaScript引擎研究(三)垃圾回收器的实现

V8垃圾回收机制简介 V8垃圾回收器的实现,是V8高效的一个非常重要的原因. V8在运行时自动回收不再需要使用的对象内存,也即是垃圾回收. V8使用了全暂停式(stop-the-world).分代式(generational).精确(accurate)等组合的垃圾回收机制,来确保更快的对象内存分配.更短的垃圾回收时触发的暂停以及没有内存碎片. V8的垃圾回收有如下几个特点: 当处理一个垃圾回收周期时,暂停所有程序的执行. 在大多数垃圾回收周期,每次仅处理部分堆中的对象,使暂停程序所带来的影响降至

Unit04: JavaScript 概述 、 JavaScript 基础语法 、 流程控制

Unit04: JavaScript 概述 . JavaScript 基础语法 . 流程控制 my.js function f3() { alert("唐胜伟"); } demo1.html  演示点击按钮,弹出提示框 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <

【JS】JavaScript引擎的内部执行机制

 近期在复习JavaScript,看到setTimeout函数时.想起曾经刚学时,在一本书上看过setTimeout()里的回调函数执行的间隔时间有昌不是后面设置的值.曾经没想太多.网上看了JS大神的解释,整理记录下JavaScript引擎的内部执行机制. 首先看一段小程序: <script> alert('第1'); setTimeout(function(){alert('第2');}, 2000); alert('第3'); </script> 输出顺序是:第1.第3,第

JavaScript 引擎性能比较之二Google V8

还是和上一篇一样的 javascript arraysort.js, 现在由 V8 来执行, 看看测试结果如何 function random_str() { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for( var i=0; i < 8; i++ ) text += possible.charAt(Math.

V8 JavaScript引擎研究(一)简介

V8 JavaScript引擎简介 V8是Google公司的高效JavaScript引擎.它使用C++开发,完全开源,最著名的是使用在Chrome浏览器中作为JavaScript解析引擎. V8实现了ECMAScript-262标准,可以运行在Windows(XP及以上).Mac OS X(10.5及以上)以及使用IA-32.x64或ARM处理器的Linux系统. V8通常使用在浏览器中作为JavaScript的解析引擎,也可以嵌入到任何应用程序中使用. 基于C++的应用程序通过V8可以暴露任何