Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
justjavac opened this issue Jul 25, 2017 · 0 comments
Open

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

justjavac opened this issue Jul 25, 2017 · 0 comments
Labels

Comments

@justjavac
Copy link
Owner

在 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。

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

Test Dictionary Mode

速度差了近 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

在视频 https://www.youtube.com/watch?v=hWhMKalEicY 中,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 将对象的字典模式变成了快速模式。https://v8.paulfryzel.com/docs/master/classv8_1_1internal_1_1_j_s_object_a663c5f054f780e77e595402eef1c4d1e_cgraph.svg

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

相关阅读:


欢迎订阅我的微信公众帐号 (justjavac-blog):

justjavac 微信公众帐号

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant