V8引擎隐藏类的概念

244 阅读3分钟

前言

最近看到了一个关于优化的面试题

const obj1 = {     |      const obj1 = {
    a: 1,          |         a: 1,
    b: 2           |         b: 2
}                  |      }
                   |          
const obj2 = {     |      const obj2 = {
    a: 1,          |         b: 2,
    b: 2           |         a: 1
}                  |      }

左右两边都是定义obj1obj2对象,右边的obj2a属性和b属性换了位置,那么左右两边有什么区别呢?看到这道题确实很疑惑,这里是跟性能优化相关的,考察的知识是v8的隐藏类

隐藏类(Hidden Classes)

因为Javascript是一种动态编程语言,这意味着对象在初始化后仍然可以对其属性进行增删操作,才会有了隐藏类的概念,其实隐藏类的作用是为了优化属性的访问速度,下面通过例子来看看隐藏类做了些什么

class A{
    constructor(name, age){
        this.name = name
        this.age = age
    }
}

const a = new A('zs', 19)
  1. V8在执行第一行代码的时候会生成一个Hidden class 0的类
Hidden class 0{

}

在此时还没有声明任何的属性,所以Hidden class 0现在为空

  1. V8在执行第二行代码的时候会生成一个Hidden class 1的类,在Hidden class 0中添加内存偏移量指针,指向Hidden class 1
Hidden class 1{
  public string name;
}

        ↑
        |
        |

Hidden class 0{
  if add 'name' transition to class 1
}
  1. V8在执行第三行代码的时候会生成一个Hidden class 2的类,在Hidden class 1中添加内存偏移量指针,指向Hidden class 2,当前的Hidden class 2则代表最终被实例化出来的对象
Hidden class 2{
  public string age;
}

        ↑
        |
        |

Hidden class 1{
  public string name;
  if add 'age' transition to class 2
}
  1. 他们可以彼此间间隔固定的偏移量储存在一段连续的内存空间中,最终的关系:
Hidden class 2{
  public string age;
}

        ↑
        |
        |

Hidden class 1{
  public string name;
  if add 'age' transition to class 2
}

        ↑
        |
        |

Hidden class 0{
  if add 'name' transition to class 1
}

通过循环实例化

那么隐藏类的好处在哪里呢,哪里实现了性能优化?下面我们通过一个for循环创建实例对象

for(let i = 0; i < 1000; i++){
    new A(`user${i}`, i)
}

当第一次循环,会创建出class 0 class 1 class 2,当进入到第二次循环,V8会去查找有没有能代表当前代码的隐藏类,如果有则直接使用,那么这个循环无论循环多少次,都只会创建class 0 class 1 class 2 者这三个隐藏类,可以一直复用

什么情况下会重新创建隐藏类

我们通过改造class A

class A{
    constructor(name, age){
        this[name] = name
        this.age = age
    }
}

for(let i = 0; i < 1000; i++){
    new A(`user${i}`, i)
}

当执行第一次循环时,会生成一个空的Hidden class 0

Hidden class 0{
    
}

但是执行到第二行代码时,因为属性名是动态变化的,这个时候Hidden class 1生成的属性名为user0

Hidden class 1{
  public string user0;
}

        ↑
        |
        |

Hidden class 0{
  if add 'user0' transition to class 1
}

当执行第三行代码时,还是会创建一个Hidden class 2

Hidden class 2{
  public string age;
}

        ↑
        |
        |

Hidden class 1{
  public string user0;
  if add 'age' transition to class 2
}

        ↑
        |
        |

Hidden class 0{
  if add 'user0' transition to class 1
}

到目前为止和方案一消耗的性能是一致的,都是创建了三个隐藏类,但是走第二次循环时,就不一样了

Hidden class 2{
  public string age;
}

        ↑
        |
        |

Hidden class 1{
  public string user1;
  if add 'age' transition to class 2
}

        ↑
        |
        |

Hidden class 0{
  if add 'user1' transition to class 1
}

因为属性名发生了变化,V8找不到能代表当前代码的隐藏类所以需要重新生成隐藏类,那么依次类推,我们循环了多少次,就要生成n个隐藏类,相互之间起不到复用的效果,所以两个例子所消耗的性能差别是很大的

优化建议

  1. 保证以相同的顺序实例化对象属性,这样可以保证它们共享相同的隐藏类。
  2. 在初始化属性时不要使用动态属性名,推荐使用this.name = name这种方式

面试题解答

同样都是生成obj1obj2,但是左边的结构在创建obj2时复用了执行obj1生成的隐藏类,右侧结构因为ab的属性定义顺序不相同,则导致了找不到对应的隐藏类,从而要创建出一个新的隐藏类,所以结论就是左侧的代码性能会好于右侧的代码