V8-学习3-渲染流水线-运行时/机器代码/堆栈/字节码/隐藏类/内联缓存

131 阅读4分钟

1.运行时环境

宿主环境

  • 浏览器
    • 消息循环系统
    • 全局变量
      • window
      • dom
    • webAPI
    • 堆栈
  • node
  • 宿主是细胞,v8是病毒
  • 提供给运行时的各种资源

v8环境

  • 实现ECMAScript标准的
    • 标准对象
      • Function
      • Object
      • Array
      • String
    • 标准函数
      • Math
  • 垃圾回收
  • 协程

执行流程

  1. 打开一个tab页面
  2. 创建一个渲染进程
  3. 初始化v8
  4. 宿主初始化堆栈
      • 线性结构,连续空间
      • 存放东西
        • 原生类型
        • 引用对象地址
        • 函数执行状态
        • this
      • 每个函数执行完都会自动销毁
      • 树形结构,非连续空间,可以存储更大的数据
    • 执行过程的数据都会存储在 栈和堆中
  5. 初始化全局作用域
    • 通过代码结构,生成词法作用域scope,把关键的变量,函数声明的对象先保存到 堆栈里
    • 例子

var x = 5  //全局作用域
{
  let y = 2  //局部作用域
  const z = 3  //局部作用域
}

//执行代码时候,依赖与当前的执行上下信息规则顺序查找
// 1. 词法环境
// 2. 语法环境,
// 3. 最后 this.outer指向外部对象 函数定义时的关系
  1. 初始化全局执行上下文
    1. 变量环境
      • webAPI
        • setTimeout
        • XMLHttpRequest
    2. 词法环境
    3. this
      • window
    • 在V8中不会被销毁,一直存储在堆里
    • 函数直接执行完就销毁。
    • 执行代码如果包含 全局的赋值也会保存在全局执行上下文
  2. 构建事件循环系统
    • 优势 :正常进程执行完后就要销毁,会浪费很多已经初始化的资源
    • 初始化主线程,宿主主线程 执行代码
    • 通过一个死循环,一直读取任务并执行

2.机器代码

内存

  • 每个地址都是唯一
  • 一个内存地址可能对应很长的内存块
  • 每段二进制代码都对应有内存地址

cpu

包含的寄存器

  • PC寄存器
    • 记录当前要执行的内存地址
  • 寄存器组
    • ecx
  • 通用寄存器,用于对应内存端
    • rbp寄存器
    • rsp寄存器
    • 代替直接访问内存,提高读写的效率
    • 容量小但读写快

执行二进制

  • c语言编译生成机器代码
  • 机器代码反编译可得到 汇编代码
    • 48 89 e5 movq %resp,%rbp
    • 左边为机器代码16进制, 右边为汇编代码

执行流程

  1. 二进制代码加载到内存,同步把第一条内存地址写入PC寄存器
  2. cpu一行行执行,cpu生命钟
    1. 取出一条指令
      • 根据PC寄存读取内存地址
      • 同时把下一个内存地址更新到 PC寄存器
    2. 分析指令
      • 识别不同的指令
movl  %ecx , -8(%rpb) //加载指令 内存数据加载cpu 
movl  -8(%rpb) , %ecx  //存储指令 cpu数据存储到内存 
addl %eax ,%ecx  //更新指令  寄存器 eax 和 ecx 中的值传给 ALU,ALU在返回到CPU
jmp -33 <_main+0x36 //跳转指令  直接修改 PC寄存器,下次cpu执行时候就会按最新地址执行

// IO读写指令
outb %0 , %1 // 写数据
inb %0 , %1 // 读数据

pushq //数据压到栈顶
%ecx  //属于cpu 的寄存器组
3. 执行指令

3. cpu执行一条指令

3.堆和栈

函数调用的特点

  • 可以被调用
    • 互相调用
  • 数据隔离
    • 函数作用域
    • 执行完自动销毁
  • 先进后出
    • 最早调用拥有最长的生命时长

  • 满足函数调用的特点
  • 连续空间
    • 创建和销毁都很快
  • 执行流程
    • 入栈
      • 变量会依次自底向上入栈
    • 出栈
      • 自顶往下 出栈
  • 寄存器
    • esp
      • 永远指向当前栈顶的指针
      • 实现方法跳转,恢复现场
    • epb
      • 记录上一次调用函数是的栈顶指针

例子

int add(num1,num2){
    int x = num1;
    int y = num2;
    int ret = x + y;
    return ret;
}
int main()
{
    int x = 5;
    int y = 6;
    x = 100;
    int z = add(x+y);
    return z;
}

// 栈执行顺序
// 0000f101  add
// 0000f81  main栈帧  0000f71
// 0000f71   main

  1. 当main要调用 add的时候,main的栈顶插一条到内存栈顶
  2. 把新插的数据保存到epb
  3. 当main要调用 add的时候,eps上移到0000f101
  4. 当add方法结束的时候,把栈顶指针esp地址改为 之前保存的epb,即0000f81
  5. 同时epb 的写入0000f81对应的0000f71
  • 栈帧
    • 对应为执行完成的函数
    • 保留函数返回地址和局部变量

  • 不连续但支持更大的空间
  • 分配一块区域,并返回引用地址,地址保存的栈中

4.惰性解析

js不一次性编译的原因

  1. 如果js文件太大,需要较长时间去编译
  2. 执行完的机器或中间代码会放到内存里,占用内存空间

惰性解析

  1. 遇到函数声明,跳过内部代码的编译
  2. 只生成顶层代码的AST和字节码

例子

function foo(a,b) {
    var d = 100
    var f = 10
    return d + f + a + b;
}
var a = 1
var c = 4
foo(1, 5)
// 这里的只是将 声明foo函数转为函数对象,内部的不做处理
// 只有当执行foo的时候,才编译foo方法

闭包

大三特征

  1. 函数内运行定义函数
  2. 函数访问父函数的变量
    • 使用词法作用域和作用域链访问变量
    • 词法作用域是v8在扫描代码的时候,根据函数的位置已经确定的
  3. 函数可以做返回参数

例子

function foo() {
    var d = 20
    return function inner(a, b) {
        const c = a + b + d
        return c
    }
}
const f = foo()

预解析器

提前扫描代码,只处理当前一个层级

  1. 检查代码语法
  2. 检查函数内部是否引用外部变量,会把变量复制到堆closeure里

5.字节码

早期V8

编译器

  1. 基线编译器:把代码直接编译成没有优化过的机器代码
  2. 优化编译器:将一部分频繁重复执行的代码编译成优化过的机器代码

流程

  1. 代码转化成AST
  2. 使用基线编译器编译成没有优化过的机器代码
  3. 执行机器代码
  4. 当遇到重复执行的代码,会把代码标记为HOT,并会触发优化编译器,把,把代码编译成优化过的机器代码
  5. 执行优化的机器代码
  6. 如果优化代码变更了,会还原为基线编译器进行编译和执行

机器代码缓存

  1. 内存缓存
    • 每次执行,通过源文件字符串做索引缓存到内存里,下次直接复用
  2. 硬盘缓存
    • 关闭浏览器下次也能复用
  • 缺点
    • 机器代码文件过大,占用内存过高,
    • 所以早期只缓存了顶层的代码,但是自执行函数产生的闭包数据,无法被执行而提前缓存

最新V8

使用字节码的优势

  1. 解决跨平台不同cpu指令要需要生成不同二进制代码的复杂度,统一一个字节码入口,再针对不同平台编写命令即可
  2. 文件体积更小,占用内存也小
  3. 启动速度更快,因为生成字节码速度比较快

生成字节码流程

例子

function add(x, y) {
  var z = x+y
  return z
}
console.log(add(1, 2))
  1. 代码解析parser
    1. 参数的声明 (PARAMS)
    2. 变量声明节点 (DECLS)
    3. x+y 的表达式节点
    4. RETURN 节点
  2. 生成AST和作用域信息
    • 执行阶段,作用域中的变量会指向堆和栈中相应的数据
  3. 通过Ignition 的解释器中的BytecodeGenerator(字节码生成器) 把AST 转化成 字节码
  4. 通过 Ignition解释器,解释执行字节码,同事配合当前的作用域信息

解释器

定义

模拟物理机器来执行字节码的,比如可以实现如取指令、解析指令、执行指令、存储数据等

类型

  1. 基于栈
    • 使用栈来处理中间数据,参数,运算结果,变量等
    • 也包含少量寄存器
    • 使用语言
      • java虚拟机
      • .net虚拟机
      • 早期的V8虚拟机
  2. 基于寄存器
    • 使用寄存器来处理中间数据,参数,运算结果,变量等
    • 也包含少量堆栈
    • 使用语言
      • 现代的js虚拟器
  3. 差异
    • 指令集

字节码执行

例子

function add(x, y) {
  var z = x+y
  return z
}
console.log(add(1, 2))

字节码使用4个模块

  1. 内存中存储字节码
  2. 通用寄存器
    • PC寄存器
    • 累加器
    • 寄存器r1,r,2,r3,r4
  3. 栈,当前栈顶指针,参数
    • a1,a2,a3,a4
  4. 堆,闭包,对象信息

字节码指令

  • Ldar
    • LoaD Accumulator from Register
    • 寄存器保存到累加器
  • Star
    • Store Accumulator Register
    • 累加器中的值保存到某个寄存器
  • Add
    • Add a1 [0]
    • 从 a1 寄存器加载值并将其与累加器中的值相加
    • [0] 为反馈向量的信息
  • LdaSmi
    • LdaSmi [5]
    • 加载小整数5到累加器上
  • Return
    • 结束函数执行
    • 返回控制权给主程序
    • 返回累加器的值
  • StackCheck
    • 检查内存栈是否溢出

6.隐藏类

静态语言

  • 对象结构是提前确定的
  • 使用相对的偏移量快速索引属性
//c++
strut Obj {
 int a;
 int b; 
}
Obj obj;
obj.a = 1;
obj.b = 2;

js是动态语言

  • 对象的属性可以随时新增删除,无法提前确定
var obj = {}
obj.x = 1
obj.y = 2
  • 可以通过hidden Class 隐藏类模拟实现
  • 通过在每个对象中新增map属性实现

隐藏类Hidden Class

特点

  1. 包含所有属性
  2. 各个类型相对于对象的偏移量

条件

对象的属性创建后,不能新增属性,删除属性

访问属性流程

  1. 通过先查找map隐藏类
  2. 根据通过属性找到对应的偏移值对应的偏移量,快速定位出属性
  3. 通过原对象的地址 + 偏移值 定位出属性值

例子

//              属性 - 偏移
// obj map  ->  x - 1
//              y - 2

//调试
let obj= {a:1,b:2};
%DebugPrint(obj);

d8 --allow-natives-syntax test.js


//相同的对象实例会共用同一个隐藏类
let o1= {a:1,b:2};
let o2= {a:3,b:4};

//属性顺序不一致也会创建不同的map
let o1 = {a:1,b:2}
let o1 = {b:2,a:1}

//构建变化将会重新构建map

7.内联缓存

定义

  • Inline Cache
  • 当一个函数引用的对象属性被多次使用,v8会将其缓存,下次直接使用缓存提高加载速度

原理

  1. 在函数执行过程中,把每个调用点都保存在反馈向量表里,第二次再执行的,使用上一次缓存好的信息直接使用
  2. 一个函数对应一个反馈向量表

例子

function fun(o){
    return o.a
}
var obj = {a:1}
fun(obj)

 //这里的字节码
StackCheck
LdaNamedProperty a0, [0], [0]
Return
//插入一条 LOAD  00001  offset 5 的反馈向量数据

类型

  • 单态
    • 1个隐藏类
      • state:MONO
      • 直接存值
  • 多态
    • 2-4个隐藏类
      • state:PLOY
      • 线性存储
  • 超态
    • 4个以上
      • state:MAGA
      • 字典存储

反馈向量表

多台/超态 执行逻辑

  • 当方法使用的对象值变化了,会在同一个反馈向量插入多条map和offset信息,类型变为PLOY或者 MAGA
  • 每次会比较传入对象的map 和 反馈向量的map是否一致,再取对应的offset