[V8内核系列] 开启对象属性的“fast”模式
预计阅读时间为 20 分钟,读懂时间为 120 分钟。
在 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);
}
}
相关阅读:
长按图片关注我的公众号,不定期推送前端原创文章