开启 V8 对象属性的 “fast” 模式

1,072 阅读4分钟
原文链接: zhuanlan.zhihu.com
在 Bluebird 库中有一段匪夷所思的代码(/src/util.js):
function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}
所有的 javascript 最佳实践都告诉我们不要使用 eval。更奇怪的是,这段代码却在函数 return 之后又调用了 eval,于是添加了一行注释来禁止 jshint 的警告信息。
Unreachable 'eval' after 'return'. (W027)

那么这段代码真的有那么神奇,可以加速对象中属性的访问速度吗?

在 V8 引擎中,对象有 2 中访问模式:Dictionary mode(字典模式) 和 Fast mode(快速模式)。

  • Dictionary mode(字典模式):字典模式也成为哈希表模式,V8 引擎使用哈希表来存储对象。
  • Fast mode(快速模式):快速模式使用类似 C 语言的 struct 来表示对象,如果你不知道什么是 struct,可以理解为是只有属性没有方法的 class。

当动态地添加太多属性、删除属性、使用不合法标识符命名属性,那么对象就会变为字典模式(基准测试)。

速度差了近 3 倍。

javascript 作为一名灵活的动态语言,开发者有很多种方式可以创建对象,还可以在创建完对象以后动态的添加和删除对象的属性,因此高效而灵活的表示一个对象比静态语言要困难很多。

根据 ECMA-262 标准,对象的属性都是字符串,即使使用了数字作为属性也会被转换为字符串。因此:

var b;
var a = {};
a.b = 1;
a[b] = 2;

此时 a 对象的值是:

{
  b: 1,
  undefined: 2
}

V8 中所有的变量都继承 Value。原始值都继承 Primitive,对象的类型为 Object,继承 Value,函数的类型为 Function,继承 Object。而原始值的包装类也都有各自的类型,比如 Number 的包装类是 NumberObject,也继承 Object。

Object 的属性通过 2 中方式访问:

/**
 * A JavaScript object (ECMA-262, 4.3.3)
 */
class V8_EXPORT Object : public Value {
 public:
  V8_DEPRECATE_SOON("Use maybe version",
                    bool Set(Local<Value> key, Local<Value> value));
  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context,
                    Local<Value> key, Local<Value> value);

  V8_DEPRECATE_SOON("Use maybe version",
                    bool Set(uint32_t index, Local<Value> value));
  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context, 
                   uint32_t index, Local<Value> value);

在快速模式下对象的 properties 是由 Heap::AllocateFixedArray 创建的普通 FixedArray。在字典模式下,对象的 properties 是由 NameDictionary::Allocate 创建的 NameDictionary。

在视频 V8: an open source JavaScript engine(YouTube) 中,V8 的开发者 Lars Bak 解释了对象的两种访问模式以及快速模式是如何运行的。

Vyacheslav Egorov 的 Understanding V8 中
Understanding Objects 章节也解释了 HIdden Class 是如何工作的。

当一个 JS 对象被设置为某个函数的原型的时候,它会退出字典模式:

Accessors::FunctionSetPrototype(JSObject*, Object*, void*)
↓
static JSFunction::SetPrototype(Handle<JSFunction>, Handle<Object>)
↓
static JSFunction::SetInstancePrototype(Handle<JSFunction>, Handle<Object>)
↓
static JSObject::OptimizeAsPrototype(Handle<JSObject>) 
↓
JSObject::OptimizeAsPrototype() 
↓
JSObject::TransformToFastProperties(0) 
↓
NameDictionary::TransformPropertiesToFastFor(obj, 0)

我们可以看看 V8 源码中关于 fast-prototype 的测试用例:

function test(use_new, add_first, set__proto__, same_map_as) {
  var proto = use_new ? new Super() : {};
  // New object is fast.
  assertTrue(%HasFastProperties(proto));
  if (add_first) {
    AddProps(proto);
    // 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));
  } else {
    DoProtoMagic(proto, set__proto__);
    // Still fast
    assertTrue(%HasFastProperties(proto));
    AddProps(proto);
    // After we add all those properties it went slow mode again :-(
    assertFalse(%HasFastProperties(proto));
  }
  if (same_map_as && !add_first) {
    assertTrue(%HaveSameMap(same_map_as, proto));
  }
  return proto;
}

如果觉得难懂,直接看我加粗的注释,我们可以知道:

  • 新建的对象是 fast 模式
  • 添加太多的属性,变 slow
  • 设置为其它对象的 prototype,变 fast

因此 Bluebird 代码中 f.prototype = obj 是使属性访问变快的关键。当把一个对象设置为另一个对象的 prototype 时,V8 引擎对对象的结构重新进行了优化。

V8 中关于对象的代码定义在 objects.cc 中:

void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
                                   PrototypeOptimizationMode mode) {
  if (object->IsJSGlobalObject()) return;
  if (mode == FAST_PROTOTYPE && PrototypeBenefitsFromNormalization(object)) {
    // First normalize to ensure all JSFunctions are DATA_CONSTANT.
    JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
                                  "NormalizeAsPrototype");
  }
  Handle<Map> previous_map(object->map());
  if (object->map()->is_prototype_map()) {
    if (object->map()->should_be_fast_prototype_map() &&
        !object->HasFastProperties()) {
      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
    }
  } else {
    if (object->map() == *previous_map) {
      Handle<Map> new_map = Map::Copy(handle(object->map()), "CopyAsPrototype");
      JSObject::MigrateToMap(object, new_map);
    }
    object->map()->set_is_prototype_map(true);

JSObject::MigrateSlowToFast 将对象的字典模式变成了快速模式。完整图

MigrateSlowToFast 的源码比较长,原理就是使用 FixedArray 替换了 NameDictionary。

SetPrototype 函数中有一段:

  // Set the new prototype of the object.
  Handle<Map> map(real_receiver->map());

  // Nothing to do if prototype is already set.
  if (map->prototype() == *value) return value;

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

OptimizeAsPrototype 的代码:

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);
  }
}

相关阅读: