ECMAScript 杂谈:让对象变得快起来

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

有趣的代码

蓝鸟的旧版本有一段代码非常有意思 util.js

javascript 复制代码
function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;  //
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}

几个问题

  1. toFastProperties,函数名很有意思, 快属性? 其本意是想做啥
  2. return前又用eval又是何意

新版本已经修改了, github.com/petkaantono...

解释:How does Bluebird's util.toFastProperties function make an object's properties "fast"?

简单说就是为了让访问对象的属性更快。

javascript 复制代码
function toFastProperties(obj) {
    /*jshint -W027*/     // 禁用语法检查
    function f() {}
    f.prototype = obj;  // 设置原型
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);   // 防止引擎优化
}

设置属性的函数

设置属性的函数,后面会用到。 主要是给对象设置常规属性和排序属性,至于什么是常规属性,排序属性,请继续往下看。

javascript 复制代码
function setProperties(obj, propertyCount, elementCount) {
    //添加常规属性
    for (let i = 0; i < propertyCount; i++) {
        let ppt = `property${i}`;
        obj[ppt] = ppt;
    }
    // 排序属性
    for (let i = 0; i < elementCount; i++) {
        obj[i] = `element${i}`;
    }
}

function setProperty(obj, key, value) {
    obj[key] = value;
}

排序属性 elements 和 普通属性 properties

V8官方的图片,来源 Fast properties in V8

普通属性: (普通字符串)左边的属于是普通属性,遍历输出顺序按照添加顺序。

排序属性: (数字字符串)右边的是排序属性, 遍历输出顺序按照索引大小。

示例:排序属性和普通属性

javascript 复制代码
  function MyObject() { }
  const obj = new MyObject();
  // 设置15个普通属性
  // 15 = 10个内属性 + 5个properties
  setProperties(obj, 15, 5);

示例: 遍历输出顺序

javascript 复制代码
function MyObject() {}
const obj = new MyObject();

obj['a'] = 'a'
obj['c'] = 'c'
obj['b'] = 'b'

obj[2] = '2'
obj[1] = '1'
obj[0] = '0'


console.log(...Object.keys(obj));
// 0 1 2 a c b

隐藏类 HiddenClass

类似于面向对象编程语言中的类, 目的:提高属性访问速度, 在v8中,常说为map。

下面内容翻译自 iddenClasses and DescriptorArrays

  • 隐藏类存储了关于对象的元信息,包括对象上的属性信息和一个指向对象原型的引用。
  • 其是优化编译器和内联缓存的关键 :隐藏类对于V8的优化编译器和内联缓存来说至关重要。以下是原因:
    • 优化编译器:如果优化编译器可以通过隐藏类确保对象结构的一致性,它可以直接内联属性访问。这意味着属性访问可以转化为直接内存访问操作,从而大幅提高执行效率。
    • 内联缓存:内联缓存是一种优化技术,它记录了属性在对象中的位置。如果对象的隐藏类没有变化,内联缓存可以快速定位属性,无需进行属性查找。

隐藏类的(上图的 descriptptors)描述符数组包含了有关命名属性的信息,比如属性名称以及存储值的位置。

在访问对象属性时,V8引擎使用隐藏类来确定属性在内存中的位置。由于隐藏类记录了属性的偏移量,V8可以直接通过偏移量访问属性,而不需要遍历对象的属性列表,这样可以显著提高属性访问的速度。

更多细节可以 参考 极客时间 李兵大佬的 03 | 快属性和慢属性:V8是怎样提升对象属性访问速度的?

查看隐藏类数据。

javascript 复制代码
let point = {x:100,y:200};
%DebugPrint(point);

node --allow-natives-syntax "hideclass.js"

javascript 复制代码
node --allow-natives-syntax "hideclass.js"
DebugPrint: 000000D0908C2309: [JS_OBJECT_TYPE]
 - map: 0x01e7c71f2259 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x03fe6512a321 <Object map = 000001E7C71C1239>
 - elements: 0x03698a741309 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x03698a741309 <FixedArray[0]>
 - All own properties (excluding elements): {
    00000285D42D5B29: [String] in OldSpace: #x: 100 (const data field 0), location: in-object
    00000285D42D5B41: [String] in OldSpace: #y: 200 (const data field 1), location: in-object
 }
000001E7C71F2259: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 40
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x01e7c71f2211 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0322c6dc15e9 <Cell value= 1>
 - instance descriptors (own) #2: 0x00d0908c2361 <DescriptorArray[2]>
 - prototype: 0x03fe6512a321 <Object map = 000001E7C71C1239>
 - constructor: 0x03fe65112791 <JSFunction Object (sfi = 00000285D42DCF31)>
 - dependent code: 0x03698a741239 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

具有相同结构的对象(相同顺序的相同属性)具有相同的 隐藏类

html 复制代码
<script>
    
    function MyObject() { }

    const obj = new MyObject();
    setProperty(obj, 'p1', 'v1');

    const obj2 = new MyObject();
    setProperty(obj2, 'p1', 'v1');

</script>

默认情况下,添加的每个新命名属性都会导致创建一个新的 隐藏类

注意 back_pointer 属性,表示的是由谁变化而来。

添加数组索引属性不会创建新的 隐藏类

html 复制代码
<script>

    function MyObject() {
    }

    const myObj = new MyObject();

    myObj[2] = 2;
    myObj[3] = 3;
    myObj[4] = '4';

    // myObj["gaga"] = "gaga"
    
</script>

如下看不到 back_pointer

如果你取消注释 // myObj["gaga"] = "gaga", 再试试。

内属性 In-object properties

查找普通属性的时候,V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作。

V8将部分常规属性直接存储到对象本身,成为内属性。

当常规属性少于一定数量时(默认是10),V8 就会将这些常规属性直接写进对象中,

这就是 V8 支持所谓的对象内属性,这些属性直接存储在对象本身上。这些是 V8 中最快的属性,可以直接访问。

内属性是有数量限制的,默认是10个

超过11个,多余的普通属性就会放到 properties下面

至于数量问题, V8 10.7.116 js-objects.h 855 行有提到计算公式。

至于各个参数,在 time.geekbang.org/column/arti... 也有详细的罗列。 这个数值嘛,随着v8的版本变更,也可能会变化。

快速与慢速属性 (Fast vs. slow properties)

快速属性

通常我们将存储在线性属性存储中的属性定义为"快速"。快速属性只需通过属性存储中的索引进行访问。

线性结构进行添加或者删除操作的时候,执行效率低。

慢速属性

慢属性的对象内部会有独立的非线性数据结构 (hash表) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

普通属性一定是慢属性吗?

不是的。如下, 20个中的10个在properties中,其依旧是线性的。 判断是不是快属性,v8有暴露方法 %HasFastProperties

快慢属性的转换

快转慢

  • 使用delete
  • 太多普通属性, 大于25个(node 16.10.0)
  • ......

delete

如何证明呢?

设置了15个普通属性,使用 node --allow-natives-syntax x.js执行, 会发现执行delete之前%HasFastProperties的结果是true,执行之后是false

javascript 复制代码
function MyObject() {}

const obj = new MyObject();

function setProperties(obj, propertyCount, elementCount) {
    //添加常规属性
    for (let i = 0; i < propertyCount; i++) {
        let ppt = `property${i}`;
        obj[ppt] = ppt;
    }
    // 排序属性
    for (let i = 0; i < elementCount; i++) {
        obj[i] = `element${i}`;
    }
}

setProperties(obj, 15, 0);

// node --allow-natives-syntax  "2. Fast Properties\2.3 Fast_Slow\index.js"

console.log(%HasFastProperties(obj))   //
delete obj.property10;
console.log(%HasFastProperties(obj))

运行结果:

javascript 复制代码
true
false

delete 一定 快 转 慢 吗?

答案是否定的, 隐藏类,已经提到了,有个back_pointer 属性,可以得知,当前的隐藏类从谁演变而来。

反向而行, 按照添加顺序的逆序删除,是不是就不会改变呢?

示例: 输出的结果都是true

javascript 复制代码
const utils = require('../util');

function MyObject() {}
const obj = new MyObject();

utils.setProperties(obj, 15, 0);

// node --allow-natives-syntax  "2. Fast Properties\2.4 Fast_Slow\1.1.2 fast delete.js"

console.log(%HasFastProperties(obj)) // true
delete obj.property14;
console.log(%HasFastProperties(obj)) // true
delete obj.property13;
console.log(%HasFastProperties(obj)) // true
delete obj.property12;
console.log(%HasFastProperties(obj)) // true
delete obj.property11;
console.log(%HasFastProperties(obj)) // true

一定数量的普通属性 也是慢属性

javascript 复制代码
const utils = require('../util');

function MyObject() {}
const obj = new MyObject();
const obj2 = new MyObject();

utils.setProperties(obj, 25, 0);
utils.setProperties(obj2, 26, 0);
// node --allow-natives-syntax  "2. Fast Properties\2.4 Fast_Slow\1.2 toSlow many properties.js"

console.log('obj:', %HasFastProperties(obj))   // obj: true
console.log('obj2:', %HasFastProperties(obj2)) // obj2: false

慢转快

  • 调用私有 %ToFastProperties 。
  • 设置对象成为一个函数(或对象)的原型。

%ToFastProperties

javascript 复制代码
const utils = require('../util');

function MyObject() {}
const obj = new MyObject();

utils.setProperties(obj, 26, 10);

console.log(%HasFastProperties(obj));  // false
%ToFastProperties(obj);
console.log(%HasFastProperties(obj)    // true

设置对象成为一个函数的原型

原型通常存储多实例共享的属性,并且很少动态的更改 。 因此,他们设置为快速模式,可以提升访问速度。

  • bluebird toFastProperties 文章开头
  • to-fast-propertis 库
  • MagicFunc

最近的版本,比如我的是 v16.10.0, 使用刚开始的有趣的代码段 bluebird toFastProperties

,并不能把慢转快。

换成 7.7.0也未能成功

一个很知名的库 to-fast-properties就是利用这个原理把对象转为快属性,这里直接把源码加入项目,因为源码一共就没多少行。

javascript 复制代码
const toFastproperties = require('./to-fast-properties');
function MyObject() {}

const obj = new MyObject();

function setProperties(obj, propertyCount, elementCount) {
    //添加常规属性
    for (let i = 0; i < propertyCount; i++) {
        let ppt = `property${i}`;
        obj[ppt] = ppt;
    }
    // 排序属性
    for (let i = 0; i < elementCount; i++) {
        obj[i] = `element${i}`;
    }
}

setProperties(obj, 15, 0);

// node --allow-natives-syntax  "toFast.js"

console.log(%HasFastProperties(obj))
delete obj.property10;
console.log(%HasFastProperties(obj))
toFastproperties(obj);
console.log(%HasFastProperties(obj))
javascript 复制代码
true
false
true

MagicFun

奇技淫巧学 V8 之四,迁移对象至快速模式 其有详细介绍,一山更比一山高。 这还涉及到了 多态内联缓存 PICs

javascript 复制代码
function MagicFunc(obj) {
    function FakeConstructor() {
        this.x = 0; // StoreIC(x)
    }

    // 拷贝 <Map> 作为 prototype, 不再与其它对象共享
    FakeConstructor.prototype = obj;

    // StoreIC(x) state == UNINITIALIZED
    new FakeConstructor();
    // StoreIC(x) state == PREMONOMORPHIC

    new FakeConstructor();
    // StoreIC(x) state == MONOMORPHIC
    // 隐式调用 MakePrototypesFast, 由 slow 到 fast 迁移
};

V8的操作

V8的7.0本,比如 github.com/v8/v8/blob/...

javascript 复制代码
void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
                                   bool enable_setup_mode) {
  if (object->IsJSGlobalObject()) return;
  if (enable_setup_mode && PrototypeBenefitsFromNormalization(object)) {
    // First normalize to ensure all JSFunctions are DATA_CONSTANT.
    JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
                                  "NormalizeAsPrototype");
  }
  if (object->map()->is_prototype_map()) {
    if (object->map()->should_be_fast_prototype_map() &&
        !object->HasFastProperties()) {
      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
    }
  } else {
    Handle<Map> new_map = Map::Copy(object->GetIsolate(),
                                    handle(object->map(), object->GetIsolate()),
                                    "CopyAsPrototype");
    JSObject::MigrateToMap(object, new_map);
    object->map()->set_is_prototype_map(true);

    // Replace the pointer to the exact constructor with the Object function
    // from the same context if undetectable from JS. This is to avoid keeping
    // memory alive unnecessarily.
    Object* maybe_constructor = object->map()->GetConstructor();
    if (maybe_constructor->IsJSFunction()) {
      JSFunction* constructor = JSFunction::cast(maybe_constructor);
      if (!constructor->shared()->IsApiFunction()) {
        Context* context = constructor->context()->native_context();
        JSFunction* object_function = context->object_function();
        object->map()->SetConstructor(object_function);
      }
    }
  }
}

主要看这几行代码,如果应该是should_be_fast_prototype_map() 并且不HasFastProperties(), 就调用JSObject::MigrateSlowToFast。

javascript 复制代码
    if (object->map()->should_be_fast_prototype_map() &&
        !object->HasFastProperties()) {
      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
    }

其他

to-fast-properties 库简介

周下载量千万级的库

github.com/sindresorhu...

javascript 复制代码
let fastProto = null;

// Creates an object with permanently fast properties in V8. See Toon Verwaest's
// post https://medium.com/@tverwaes/setting-up-prototypes-in-v8-ec9c9491dfe2#5f62
// for more details. Use %HasFastProperties(object) and the Node.js flag
// --allow-natives-syntax to check whether an object has fast properties.
function FastObject(object) {
	// A prototype object will have "fast properties" enabled once it is checked
	// against the inline property cache of a function, e.g. fastProto.property:
	// https://github.com/v8/v8/blob/6.0.122/test/mjsunit/fast-prototype.js#L48-L63


	// 无object参数时,
	if (fastProto !== null && typeof fastProto.property) {
		// 重置 result 为传入的object
		const result = fastProto;
		// 置空 fastProto和FastObject.prototype 
		fastProto = FastObject.prototype = null;
		return result;
	}

	// 有object参数,设置fastProto以及FastObject函数的原型
	// fastProto一般情况等于 object
	fastProto = FastObject.prototype = object == null ? Object.create(null) : object;

	// new 不传参数 走上面的分支
	return new FastObject();
}

const inlineCacheCutoff = 10;

// Initialize the inline property cache of FastObject.
for (let index = 0; index <= inlineCacheCutoff; index++) {
	FastObject();
}

export default function toFastproperties(object) {
	return FastObject(object);
}

可能大家注意到了, 初始化执行了10次? 为什么要10次? 基于node 16.10.0 测试6次以上可以,6次都不行。

--allow-natives-syntax

node启动的时候,可以带上这个参数,这其实是 v8 的一个选项,开启后,可以使用一些私有的 API。

你可以使用 node --v8-options 命令来查询v8的启动选项。

在选项中,你可以查找到 这个--allow-natives-syntax 选项。

javascript 复制代码
--allow-natives-syntax (allow natives syntax)
        type: bool  default: false

小结

普通属性:10 和 25

chrome目前情况(以后可能会变):

一般情况小于10的普通属性属于内属性,访问速度杠杠的。

25个以上的普通属性,将以非线程的结构存储,会影响访问速度。

快属性和慢属性 可以转换

快转慢:

  • delete
  • 普通属性数量

慢转快

  • %ToFastProperties
  • 设置对象成为一个函数的原型

引用

Fast properties in V8
图解 Google V8 | 03 | 快属性和慢属性:V8是怎样提升对象属性访问速度的?
fast-prototype.js
JSObject::MigrateSlowToFast
What's up with monomorphism?
V8 官方网站 的中文翻译 )
Optimization killers

相关推荐
coding随想2 小时前
JavaScript ES6 解构:优雅提取数据的艺术
前端·javascript·es6
年老体衰按不动键盘2 小时前
快速部署和启动Vue3项目
java·javascript·vue
小小小小宇2 小时前
一个小小的柯里化函数
前端
灵感__idea2 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
小小小小宇2 小时前
前端双Token机制无感刷新
前端
小小小小宇2 小时前
重提React闭包陷阱
前端
小小小小宇3 小时前
前端XSS和CSRF以及CSP
前端
UFIT3 小时前
NoSQL之redis哨兵
java·前端·算法
超级土豆粉3 小时前
CSS3 的特性
前端·css·css3
星辰引路-Lefan3 小时前
深入理解React Hooks的原理与实践
前端·javascript·react.js