likes
comments
collection
share

如何让你的Javascript 代码更快之内联缓存

作者站长头像
站长
· 阅读数 55

几个月前在网上看到介绍angular 为什么这边快的演讲。演讲里面提到了三个原因,分别是内联缓存,布隆过滤器和angular 编译器。这里我们挑选其中的一条内联缓存,进行介绍一下。

背景

实际上,除了这个演讲中介绍的angular,rxjs也提到了这个概念。这篇文章 介绍了该特性对redux 的影响。这篇文章 介绍了对react 类式组件的影响。可以看出这个特性对于开源库的影响。

静态语言与动态语言

public class Dog {
    private String name;
    private int age; 
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
Dog dog= new Dog("大狗", 12);
dog.name;
dog.age;
const dog = { name: '大狗', age: 12 };
dog.name;
dog.age;

上面的第一段代码是Java,创建了一个对象,然后访问nameage 属性。下面是对应的JavaScript 代码。Java 是静态语言,对象创建以后不会增加或者删减对象属性。因此每一个属性的偏移量在编译阶段就可以确定。但是JavaScript 是动态语言,在运行时可以通过dog.color = 'yellow'动态增加属性。所以在运行期间,JavaScript 访问对象属性的速度一定比Java 这类静态语言慢。

隐藏类

为了提高属性的访问速度,JavaScript 引擎借鉴静态语言,提出了隐藏类的概念。实际上,隐藏类是学术上的叫法,不同的JavaScript 引擎有自己的叫法,V8 称其为Maps,Chakra称为Types,JavaScriptCore称为Structures,SpiderMonkey称为Shapes。这里我们采用隐藏类这种叫法。

引入隐藏类以后,对于任意一个JavaScript 对象都有一个隐藏的map 属性,指向该对象的隐藏类。可以通过下面的代码进行查看。

const dog = {name: '大狗', age: 12};

console.log(%DebugPrint(dog));

控制台输入node --allow-natives-syntax index.js如何让你的Javascript 代码更快之内联缓存

隐藏类中存储了一些关于对象属性的信息,其中就包括offset 偏移量。除了offset 还会存储其他的一些信息,因为与这里要讨论的问题关系不大,就没有画出来了。而dog 对象只会存储属性的值,即大狗12。当我们进行dog.name 的属性访问时,会在隐藏类中找到对应的offset 是0,通过offset 就可以快速拿到属性值大狗 了。

如何让你的Javascript 代码更快之内联缓存

为什么要把隐藏类和对象分开存储呢?主要是因为多个对象可以共用一个隐藏类。

如何让你的Javascript 代码更快之内联缓存

上图所示的dogdog2具有相同的结构,就会共用一个隐藏类,通过这种方式可以减少内存浪费。

const dog = {name: '大狗', age: 12};
const dog2 = {name: '小狗', age: 3};

console.log(%HaveSameMap(dog, dog2));

控制台输入node --allow-natives-syntax index.js,返回true

隐藏类链

当我们对一个JavaScript 的属性进行删减的时候,会触发隐藏类的修改。

const dog = {name: '大狗', age: 12};
console.log(%DebugPrint(dog));
dog.color = '黄色';
console.log(%DebugPrint(dog));
DebugPrint: 000003F1AF5CD9C9: [JS_OBJECT_TYPE]
 - map: 0x0089dfd35a09 <Map(HOLEY_ELEMENTS)> [FastProperties]
 }
DebugPrint: 000003F1AF5CD9C9: [JS_OBJECT_TYPE]
 - map: 0x0089dfd3a131 <Map(HOLEY_ELEMENTS)> [FastProperties]
 }

输出结果中删除了map 以外的其他信息。

代码const dog = {name: '大狗', age: 12} 初始化dog 时,创建了隐藏类。此时,dog 的隐藏类地址是0x0089dfd35a09。然后dog.color = '黄色' 添加color 属性,又创建了一个隐藏类,并更新dogmap 指向,这时隐藏类地址是0x0089dfd3a131

图片如下所示:

如何让你的Javascript 代码更快之内联缓存

所以在代码中,一般不要频繁的增加对象属性。因为每一次的操作都会造成隐藏类的修改。应该在初始化的时候,一次性为对象声明所有的属性。

上一小节的最后提到dogdog2 共用一个的隐藏类是因为它们的结构一样。这么说其实有点不够准确。下面的代码中,虽然dogdog2 的结构一样,属性的顺序也一样。但是它们的隐藏类是不一样的。

const dog = {};
dog.name = '大黄'; 
dog.age = 12;

const dog2 = { name: '小黄', age: 2 };

console.log(%HaveSameMap(dog, dog2));

输出结果是false

如何让你的Javascript 代码更快之内联缓存

这是因为dog 初始化的时候,是一个空对象,对应空的隐藏类。然后添加name 属性,对应的隐藏类也进行创建,并且更新dogmap 指向。添加age 属性的时候同理。

dog2 初始化的时候,就直接具有nameage 属性。从最后的结果上来看,两个对象都具有相同的结构,但是它们各自的隐藏类却不是相同的。

所以如果我们希望对象之间可以共用同一个隐藏类,除了属性的顺序和结构相同以外,还需要保证它们新增属性的顺序也一致。

内联缓存

接下来我们看看,如何利用隐藏类实现函数中对象属性的快速访问。内联缓存会为每一个JavaScript 函数维护一个反馈向量表。这个反馈向量表会记录当前函数调用的一些关键的中间数据。比如下面这样一个函数

function updateNode(o) {
    let value = o.x;
    ......
}

当JavaScript 引擎执行o.x的时候,会在反馈向量表中分配一个slot ,就是往反馈向量表里面添加一行如下的记录。 如何让你的Javascript 代码更快之内联缓存 这条记录中的map 就是对象o 的隐藏类地址,offset 就是对应属性x 在对象中的偏移量。

当下次调用updateNode 函数的时候,如果传入的参数具有相同的隐藏类,就可以直接从这条记录里面找出offset 偏移量,减少了查找过程。

但是这里有个要求,就是两次调用updateNode 函数时,传入的对象一定要具有相同的隐藏类。比如下面这样。

updateNode({x: 1, y: 2});
updateNode({x: 3, y: 6});

但是如果第三次这样调用updateNode({y: 9, x: 3, z: 10})。从前面的内容,我们知道,第三次调用传入的对象的隐藏类与前两次传入对象的隐藏类不相同。于是,反馈向量表的内容,就变为了这样。

如何让你的Javascript 代码更快之内联缓存 这种情况下,JavaScript 查找属性xoffset 偏移量时,就要比之前多对比一次,因为现在记录里面有了两个隐藏类。这样的话,速度也就比之前更慢一些。所以如果想要保持最快的速度,就要保证每次传给updateNode 函数的对象都具有相同隐藏类。

反馈向量表中的state 表示当前隐藏类的个数,当只有一个隐藏类的时候,是Mono,这种情况下,速度最快。2个到4个隐藏类时是Poly,即多态。超过4个隐藏类,就是Mega,表示超态。

总结

实际上,正如文章文章 中提到的一样,对于一般的代码开发而言,正常使用JavaScript 就可以了。这种细节的优化就交给JavaScript 引擎,不需要特别关注。对于业务开发,有很多其他原因会影响项目性能,解决这些其他原因远比这点优化更加有效。当然对于angular 这种需要压榨出所有JavaScript 性能的库可能就需要关注了。

转载自:https://juejin.cn/post/7142725342308859935
评论
请登录