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

相关推荐
高木的小天才4 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚2 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能