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

Qan*_*avy 165 javascript performance v8 node.js bluebird

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

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

出于某种原因,在返回函数之后有一个声明,我不知道它为什么存在.

同样,它似乎是故意的,因为作者已经沉默了JSHint对此的警告:

'return'后无法访问'eval'.(W027)

这个功能到底是做什么的?难道util.toFastProperties真的让一个对象的属性"快"?

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

Ben*_*aum 314

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
}
Run Code Online (Sandbox Code Playgroud)

没有一个或两个小优化 - 以下所有内容仍然有效.

让我们首先讨论它的作用以及为什么它更快,然后它的工作原理.

它能做什么

V8引擎使用两个对象表示:

  • 字典模式 - 将对象存储为键 - 值映射作为哈希映射.
  • 快速模式 - 对象以结构形式存储,其中属性访问不涉及计算.

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

引擎尝试尽可能使用快速模式,通常只要执行大量属性访问 - 但有时它会被抛入字典模式.处于字典模式具有很大的性能损失,因此通常希望将对象置于快速模式.

此hack旨在从字典模式强制对象进入快速模式.

为什么它更快

在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.
}
Run Code Online (Sandbox Code Playgroud)

我们不会找到自己的代码就断言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));
Run Code Online (Sandbox Code Playgroud)

阅读和运行此测试向我们展示了这种优化确实在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);
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,JSObject::MigrateSlowToFast只需显式获取Dictionary并将其转换为快速V8对象.这是一个值得阅读和对v8对象内部的有趣见解 - 但它不是这里的主题.我仍然热烈建议你在这里阅读,因为这是学习v8对象的好方法.

如果我们看看SetPrototypeobjects.cc,我们可以看到,这就是所谓的行12231:

if (value->IsJSObject()) {
    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Run Code Online (Sandbox Code Playgroud)

反过来,FuntionSetPrototype这就是我们得到的东西.prototype =.

__proto__ =或者.setPrototypeOf也可以工作但是这些是ES6功能,Bluebird在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;
}
Run Code Online (Sandbox Code Playgroud)

直接在于Object:

InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Run Code Online (Sandbox Code Playgroud)

所以 - 我们已经从Petka编写的代码走向了裸机.这很好.

免责声明:

请记住,这是所有实现细节.像Petka这样的人是优化狂热者.永远记住过早优化是所有邪恶的根源,97%的时间.Bluebird经常做一些非常基本的事情,所以它从这些性能攻击中获得了很多 - 尽可能快地回调并不容易.您很少需要在不为库提供动力的代码中执行此类操作.

  • 这是我在一段时间内读过的最有趣的帖子.非常尊重和赞赏! (37认同)
  • 顺便说一下,由于最近的v8发生了变化,这段代码已经更新,现在你也需要实例化构造函数.所以它变得更加懒惰; d (6认同)
  • @BenjaminGruenbaum你能详细说明为什么不应该优化这个功能吗?在缩小的代码中,eval无论如何都不存在.为什么eval在非缩小代码中有用? (4认同)
  • @dherman a`1;`不会导致"去优化",一个`调试器;`可能会同样有效.好的一点是,当`eval`被传递的东西不是一个字符串时它不会对它做任何事情所以它是相当无害的 - 有点像`if(false){debugger; }` (3认同)
  • @timoxley我写了以下关于`eval`的内容(在解释发布的代码OP时的代码注释中):"通过死代码消除或进一步优化来阻止函数优化.这个代码永远不会到达但是甚至无法访问的代码导致v8不优化功能." .[这是相关的阅读](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#2-unsupported-syntax).您是否希望我进一步阐述这个主题? (2认同)