[V8内核系列] 开启对象属性的“fast”模式

预计阅读时间为 20 分钟,读懂时间为 120 分钟。


在 Bluebird 库中有一段匪夷所思的代码(/src/util.js):

  1. function toFastProperties(obj) {

  2.    /*jshint -W027*/

  3.    function f() {}

  4.    f.prototype = obj;

  5.    ASSERT("%HasFastProperties", true, obj);

  6.    return f;

  7.    eval(obj);

  8. }

所有的 javascript 最佳实践都告诉我们不要使用 eval。更奇怪的是,这段代码却在函数 return 之后又调用了 eval,于是添加了一行注释来禁止 jshint 的警告信息。

  1. Unreachable 'eval' after 'return'. (W027)

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

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

  • Dictionary mode(字典模式):字典模式也成为哈希表模式,V8 引擎使用哈希表来存储对象。

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

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

速度差了近 3 倍。

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

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

  1. var b;

  2. var a = {};

  3. a.b = 1;

  4. a[b] = 2;

此时 a 对象的值是:

  1. {

  2.  b: 1,

  3.  undefined: 2

  4. }

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

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

  1. /**

  2. * A JavaScript object (ECMA-262, 4.3.3)

  3. */

  4. class V8_EXPORT Object : public Value {

  5. public:

  6.  V8_DEPRECATE_SOON("Use maybe version",

  7.                    bool Set(Local<Value> key, Local<Value> value));

  8.  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context,

  9.                    Local<Value> key, Local<Value> value);

  10.  V8_DEPRECATE_SOON("Use maybe version",

  11.                    bool Set(uint32_t index, Local<Value> value));

  12.  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context,

  13.                   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 对象被设置为某个函数的原型的时候,它会退出字典模式:

  1. Accessors::FunctionSetPrototype(JSObject*, Object*, void*)

  2. static JSFunction::SetPrototype(Handle<JSFunction>, Handle<Object>)

  3. static JSFunction::SetInstancePrototype(Handle<JSFunction>, Handle<Object>)

  4. static JSObject::OptimizeAsPrototype(Handle<JSObject>)

  5. JSObject::OptimizeAsPrototype()

  6. JSObject::TransformToFastProperties(0)

  7. NameDictionary::TransformPropertiesToFastFor(obj, 0)

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

  1. function test(use_new, add_first, set__proto__, same_map_as) {

  2.  var proto = use_new ? new Super() : {};

  3.  // **New object is fast**.

  4.  assertTrue(%HasFastProperties(proto));

  5.  if (add_first) {

  6.    AddProps(proto);

  7.    // **Adding this many properties makes it slow**.

  8.    assertFalse(%HasFastProperties(proto));

  9.    DoProtoMagic(proto, set__proto__);

  10.    // **Making it a prototype makes it fast again**.

  11.    assertTrue(%HasFastProperties(proto));

  12.  } else {

  13.    DoProtoMagic(proto, set__proto__);

  14.    // Still fast

  15.    assertTrue(%HasFastProperties(proto));

  16.    AddProps(proto);

  17.    // After we add all those properties it went slow mode again :-(

  18.    assertFalse(%HasFastProperties(proto));

  19.  }

  20.  if (same_map_as && !add_first) {

  21.    assertTrue(%HaveSameMap(same_map_as, proto));

  22.  }

  23.  return proto;

  24. }

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

  • 新建的对象是 fast 模式

  • 添加太多的属性,变 slow

  • 设置为其它对象的 prototype,变 fast

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

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

  1. void JSObject::OptimizeAsPrototype(Handle<JSObject> object,

  2.                                   PrototypeOptimizationMode mode) {

  3.  if (object->IsJSGlobalObject()) return;

  4.  if (mode == FAST_PROTOTYPE && PrototypeBenefitsFromNormalization(object)) {

  5.    // First normalize to ensure all JSFunctions are DATA_CONSTANT.

  6.    JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,

  7.                                  "NormalizeAsPrototype");

  8.  }

  9.  Handle<Map> previous_map(object->map());

  10.  if (object->map()->is_prototype_map()) {

  11.    if (object->map()->should_be_fast_prototype_map() &&

  12.        !object->HasFastProperties()) {

  13.      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");

  14.    }

  15.  } else {

  16.    if (object->map() == *previous_map) {

  17.      Handle<Map> new_map = Map::Copy(handle(object->map()), "CopyAsPrototype");

  18.      JSObject::MigrateToMap(object, new_map);

  19.    }

  20.    object->map()->set_is_prototype_map(true);

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

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

在 SetPrototype 函数中有一段:

  1.  // Set the new prototype of the object.

  2.  Handle<Map> map(real_receiver->map());

  3.  // Nothing to do if prototype is already set.

  4.  if (map->prototype() == *value) return value;

  5.  if (value->IsJSObject()) {

  6.    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));

  7.  }

OptimizeAsPrototype 的代码:

  1. void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {

  2.  if (object->IsGlobalObject()) return;

  3.  // Make sure prototypes are fast objects and their maps have the bit set

  4.  // so they remain fast.

  5.  if (!object->HasFastProperties()) {

  6.    MigrateSlowToFast(object, 0);

  7.  }

  8. }

相关阅读:


长按图片关注我的公众号,不定期推送前端原创文章


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值