Bluebird 的 util.toFastProperties 函数如何使对象的属性“快速”?

在 Bluebird 的 util.js文件中,它有以下功能:

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

出于某种原因,在 return 函数后面有一个语句,我不知道为什么会有这个语句。

而且,这似乎是有意为之,因为作者已经让 JSHint 对此发出的警告保持沉默:

在“返回”之后不可达到的“ eval”

这个函数到底是做什么的? util.toFastProperties真的能让一个对象的属性“更快”吗?

我在 Bluebird 的 GitHub 存储库中搜索了源代码中的任何注释或问题列表中的解释,但是没有找到任何注释。

12090 次浏览

2017年更新: 首先,为了方便今天的读者,下面是一个使用 Node 7(4 +)的版本:

function enforceFastProperties(o) {
function Sub() {}
Sub.prototype = o;
var receiver = new Sub(); // create an instance
function ic() { return typeof receiver.foo; } // perform access
ic();
ic();
return o;
eval("o" + o); // ensure no dead code elimination
}

没有一个或两个小的优化-所有以下仍然有效。


让我们首先讨论一下它是做什么的,为什么它更快,为什么它能工作。

它的作用

V8引擎使用两种对象表示法:

  • Dictionary mode -其中对象作为键值映射存储为 散列表
  • 快速模式 -在这种模式下,对象像 结构一样存储,在这种模式下,属性访问不涉及任何计算。

下面是展示速度差异的 一个简单的演示。这里我们使用 delete语句强制对象进入慢字典模式。

引擎尽可能使用快速模式,通常在执行大量属性访问时使用快速模式,但有时会进入字典模式。处于字典模式有很大的性能损失,因此一般来说,将对象置于快速模式是可取的。

这种黑客行为的目的是强制对象从字典模式进入快速模式。

为什么它更快

在 JavaScript 原型中,通常存储在许多实例之间共享的函数,并且很少动态地进行大量更改。出于这个原因,我们非常希望它们处于快速模式,以避免每次调用函数时的额外损失。

因此,v8将很高兴地将函数的 .prototype属性的对象置于快速模式,因为它们将被作为构造函数调用该函数创建的每个对象共享。这通常是一个聪明而令人满意的优化。

它是如何工作的

让我们首先浏览一下代码,看看每一行代码是做什么的:

function toFastProperties(obj) {
/*jshint -W027*/ // suppress the "unreachable code" error
function f() {} // declare a new function
f.prototype = obj; // assign obj as its prototype to trigger the optimization
// assert the optimization passes to prevent the code from breaking in the
// future in case this optimization breaks:
ASSERT("%HasFastProperties", true, obj); // requires the "native syntax" flag
return f; // return it
eval(obj); // prevent the function from being optimized through dead code
// elimination or further optimizations. This code is never
// reached but even using eval in unreachable code causes v8
// to not optimize functions.
}

我们不用 自己找代码来断言 v8进行了这种优化,我们可以用 阅读 v8单元测试代替:

// Adding this many properties makes it slow.
assertFalse(%HasFastProperties(proto));
DoProtoMagic(proto, set__proto__);
// Making it a prototype makes it fast again.
assertTrue(%HasFastProperties(proto));

阅读和运行这个测试向我们展示了这个优化确实在 v8中起作用。但是,如果能看到这一点就好了。

如果我们检查 objects.cc,我们可以找到以下函数(L9925) :

void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {
if (object->IsGlobalObject()) return;


// Make sure prototypes are fast objects and their maps have the bit set
// so they remain fast.
if (!object->HasFastProperties()) {
MigrateSlowToFast(object, 0);
}
}

现在,JSObject::MigrateSlowToFast只是显式地接受 Dictionary 并将其转换为一个快速的 V8对象。这是一本值得一读的书,也是对 v8对象内部的一个有趣的了解——但这里不讨论这个主题。我仍然热烈推荐 你在这里读到的,因为它是了解 v8对象的好方法。

如果我们查看 objects.cc中的 SetPrototype,我们可以看到它在第12231行中被调用:

if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}

它又被 FuntionSetPrototype调用,这就是我们在 .prototype =中得到的。

执行 __proto__ =.setPrototypeOf也可以,但这些是 ES6函数,蓝鸟运行在 Netscape 7以来的所有浏览器上,因此这里不可能简化代码。例如,如果我们检查 .setPrototypeOf,我们可以看到:

// ES6 section 19.1.2.19.
function ObjectSetPrototypeOf(obj, proto) {
CHECK_OBJECT_COERCIBLE(obj, "Object.setPrototypeOf");


if (proto !== null && !IS_SPEC_OBJECT(proto)) {
throw MakeTypeError("proto_object_or_null", [proto]);
}


if (IS_SPEC_OBJECT(obj)) {
%SetPrototype(obj, proto); // MAKE IT FAST
}


return obj;
}

直接在 Object频道播出:

InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));

所以,我们已经从佩特卡写的代码走到了赤裸裸的金属。这是很好的。

免责声明:

记住这些都是实现细节。像佩特卡这样的人都是优化狂。永远记住,过早优化在97% 的情况下都是万恶之源。Bluebird 经常做一些非常基本的事情,所以它从这些性能技巧中获益匪浅——像回调一样快并不容易。很少必须在不支持库的代码中执行类似的操作。

2021年的现实(NodeJS 版本12 +)。 似乎做了一个巨大的优化,删除字段和稀疏数组的对象不会变慢。还是我错过了史密斯?

// run in Node with enabled flag
// node --allow-natives-syntax script.js


function Point(x, y) {
this.x = x;
this.y = y;
}


var obj1 = new Point(1, 2);
var obj2 = new Point(3, 4);
delete obj2.y;


var arr = [1,2,3]
arr[100] = 100


console.log('obj1 has fast properties:', %HasFastProperties(obj1));
console.log('obj2 has fast properties:', %HasFastProperties(obj2));
console.log('arr has fast properties:', %HasFastProperties(arr));

都是真的

obj1 has fast properties: true
obj2 has fast properties: true
arr has fast properties: true

// run in Node with enabled flag
// node --allow-natives-syntax script.js


function Point(x, y) {
this.x = x;
this.y = y;
}


var obj2 = new Point(3, 4);
console.log('obj has fast properties:', %HasFastProperties(obj2)) // true
delete obj2.y;
console.log('obj2 has fast properties:', %HasFastProperties(obj2)); //true


var obj = {x : 1, y : 2};
console.log('obj has fast properties:', %HasFastProperties(obj))  //true
delete obj.x;


console.log('obj has fast properties:', %HasFastProperties(obj)); //fasle

函数和对象看起来不同

我是 V8开发人员。接受的答案是一个伟大的解释,我只想强调一件事: 所谓的“快”和“慢”属性模式是不幸的用词不当,他们各有利弊。下面是对各种操作性能的一个(略微简化的)概述:

结构类属性结构类属性结构类属性 字典属性
向对象添加属性 -- +
删除属性 --- +
第一次读/写属性 - +
读/写,缓存的,单态的 +++ +
读/写,缓存,几种形状 ++ +
读/写,缓存,多种形状 -- +
俗称 “快” “慢”

因此,正如您所看到的,字典属性实际上对于这个表中的大多数行都更快,因为它们不关心您做什么,它们只是以可靠的(尽管不是破记录的)性能处理所有事情。类结构属性在某种特定情况下非常迅速(读/写现有属性的值,在这种情况下代码中的每个位置只能看到非常少的不同对象形状) ,但它们为此付出的代价是所有其他操作,特别是那些添加或删除属性的操作,变得 很多更慢。

碰巧类结构属性(struct-like properties)具有巨大优势的特殊情况(+++)对于许多应用程序的性能来说尤其频繁而且非常重要,这就是为什么它们获得了“快速”的绰号。但重要的是要认识到,当您 delete属性和 V8将受影响的对象切换到字典模式时,那么它并不是愚蠢或试图惹人讨厌: 而是尝试为您正在做的事情提供尽可能好的性能。我们在过去已经着陆的补丁,通过使更多的对象在适当的时候更快地进入字典(“缓慢”)模式,已经实现了显著的性能 改进

现在,可以发生了这样的情况,您的对象通常会从结构化属性中受益,但是您的代码所做的某些事情会导致 V8将它们转换为字典属性,您想撤消这一点; Bluebird 就有这样的情况。尽管如此,toFastProperties这个名字的简单性还是有点误导人; 更准确(尽管不方便)的名字应该是 spendTimeOptimizingThisObjectAssumingItsPropertiesWontChange,这意味着操作本身是昂贵的,而且它只在某些有限的情况下才有意义。有人拿走了结论“哦,这是伟大的,所以我现在可以高兴地删除属性,只是调用 toFastProperties之后,每次”,那将是一个主要的误解,并造成相当糟糕的性能下降。

如果你坚持一些简单的经验法则,你将永远没有理由去强迫任何内部对象表示的改变:

  • 使用构造函数,并初始化构造函数中的所有属性。(这不仅有助于引擎,还有助于代码的可理解性和可维护性。考虑到 TypeScript 并不强制这样做,但是强烈鼓励这样做,因为它有助于工程生产力。)
  • 使用类或原型来安装方法,而不是仅仅将它们安装在每个对象实例上。(同样,这是一个常见的最佳实践,原因有很多,其中之一就是它更快。)
  • 避免 delete。当属性来来去去时,更喜欢使用 Map而不是 ES5时代的“ object-as-map”模式。当一个对象可以切换进入和退出某种状态时,比起添加和删除指示符属性,更喜欢布尔(或等效)属性(例如 o.has_state = true; o.has_state = false;)。
  • 当涉及到性能,测量,测量,测量。在你开始投入时间改进性能之前,先分析一下你的应用程序,看看热点在哪里。当您实现一个您希望能够使事情变得更快的更改时,验证 用你真正的应用程序(或者非常接近它的东西; 而不仅仅是一个10行的微基准测试!)真的很有帮助。

最后,如果你的团队领导告诉你“我听说有‘快速’和‘慢速’属性,请确保我们所有人都是‘快速’的”,然后把他们指向这个帖子: -)