justjavac / justjavac/v8-source-read

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

Open
#4 0 comments 0 reactions 0 assignees View on GitHub
归档
Dominant language
JavaScript
Stars
44
Forks
7
PR merge metrics
No merged PRs in 30d

Description

在 Bluebird 库中有一段匪夷所思的代码([/src/util.js](https://github.com/petkaantonov/bluebird/blob/7454401269cfa47e5b001354388c062509103de7/src/util.js#L180-L187)):

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

当动态地添加太多属性、删除属性、使用不合法标识符命名属性,那么对象就会变为字典模式([基准测试](https://jsperf.com/test-dictionary-mode))。

![Test Dictionary Mode](https://user-images.githubusercontent.com/359395/28569884-53670310-716e-11e7-810b-586957bfdcd3.png)

速度差了近 3 倍。

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

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

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

此时 `a` 对象的值是:

```js
{
b: 1,
undefined: 2
}
```

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

![](https://user-images.githubusercontent.com/359395/28569940-7bf2ec18-716e-11e7-9d81-fe3f69e9f073.png)

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

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

V8_DEPRECATE_SOON("Use maybe version",
bool Set(uint32_t index, Local value));
V8_WARN_UNUSED_RESULT Maybe Set(Local context,
uint32_t index, Local value);
```

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

在视频 `https://www.youtube.com/watch?v=hWhMKalEicY` 中,V8 的开发者 Lars Bak 解释了对象的两种访问模式以及快速模式是如何运行的。

Vyacheslav Egorov 的 [Understanding V8](http://mrale.ph/s3/nodecamp.eu/#41) 中 Understanding Objects 章节也解释了 Hidden Class 是如何工作的。

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

```
Accessors::FunctionSetPrototype(JSObject*, Object*, void*)

static JSFunction::SetPrototype(Handle, Handle)

static JSFunction::SetInstancePrototype(Handle, Handle)

static JSObject::OptimizeAsPrototype(Handle)

JSObject::OptimizeAsPrototype()

JSObject::TransformToFastProperties(0)

NameDictionary::TransformPropertiesToFastFor(obj, 0)
```

我们可以看看 V8 源码中关于 [fast-prototype](https://github.com/v8/v8/blob/d52280b1a7a867ffb350c4f193cf8692861855dd/test/mjsunit/fast-prototype.js#L57-L82) 的测试用例:

```js
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](https://github.com/v8/v8/blob/master/src/objects.cc#L12228-L12264) 中:

```cpp
void JSObject::OptimizeAsPrototype(Handle 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 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 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`。

![](https://v8.paulfryzel.com/docs/master/classv8_1_1internal_1_1_j_s_object_a663c5f054f780e77e595402eef1c4d1e_cgraph.svg)

[MigrateSlowToFast](https://github.com/v8/v8/blob/3235f3f8b5930de07a240f61386f21d55040dbf8/src/objects.cc#L4617-L4751) 的源码比较长,原理就是使用 `FixedArray` 替换了 `NameDictionary`。

在 [SetPrototype](https://github.com/v8/v8/blob/3235f3f8b5930de07a240f61386f21d55040dbf8/src/objects.cc#L12188) 函数中有一段:

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

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

if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle::cast(value));
}
```
[OptimizeAsPrototype](https://github.com/v8/v8/blob/3235f3f8b5930de07a240f61386f21d55040dbf8/src/objects.cc#L9936-L9944) 的代码:

```cpp
void JSObject::OptimizeAsPrototype(Handle 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);
}
}
```

## 相关阅读:

- [JavaScript 函数式编程存在性能问题么?](https://www.zhihu.com/question/54637225/answer/140362071)

---------------------

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

![justjavac 微信公众帐号](https://user-images.githubusercontent.com/359395/28569661-61a61ad4-716d-11e7-84f5-28d1a7ed2090.png)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.