a.b.c.d 和 a['b']['c']['d'] 的性能比较

Date published: 3-Feb-2020
Date modified: 3-Feb-2020
1 min read / 156 words
Author: Tiny

JavaScript

很容易想到的一个方法是通过写一个实例来比较运行时间,通过实例来看,我发现两者差距非常小,可以忽略不计。


function compare(times) {
let a = { key: {} };
let temp = a;
for (let i = 0; i < times; i++) {
let tmp = temp['key'];
tmp['key'] = {};
temp = tmp;
}
temp['key']['key'] = 'surprise';
let d0 = new Date();
let i = a;
while (i['key'] !== 'surprise') {
i = i['key'];
}
console.log('[] time', new Date() - d0);
let d1 = new Date();
let ii = a;
while (ii.key !== 'surprise') {
ii = ii.key;
}
console.log('. time', new Date() - d1);
}
compare(10000000); // [] time 62 . time 51
compare(10000001); // [] time 60 . time 62
compare(10000002); // [] time 61 . time 62
compare(10000003); // [] time 62 . time 62

于是我希望通过底层来比较下这两者到底有什么区别。

我想到的第一个办法是将代码转换成 AST,看看结果有什么不同。

借助于 @babel/parser,并且打开 tokens 选项,可以看到词法分析阶段产生的 token 有什么区别。

const parser = require('@babel/parser');
const code1 = `a.b.c.d`;
const code2 = `a['b']['c']['d']`;
const ast1 = parser.parse(code1, { tokens: true });
const ast2 = parser.parse(code2, { tokens: true });
console.log(ast1, ast2);

词法分析

a.b.c.d

Token: [
0: Token {type: TokenType, value: "a",}
1: Token {type: TokenType {label: ".",} }
2: Token {type: TokenType, value: "b",}
3: Token {type: TokenType {label: ".",} }
4: Token {type: TokenType, value: "c",}
5: Token {type: TokenType {label: ".",} }
6: Token {type: TokenType, value: "d",}
7: Token {type: TokenType {label: "eof",} }
]

a['b']['c']['d']

Token: [
0: Token {type: TokenType, value: "a",}
1: Token {type: TokenType {label: "[",} }
2: Token {type: TokenType, value: "b",}
3: Token {type: TokenType {label: "]",} }
4: Token {type: TokenType {label: "[",} }
5: Token {type: TokenType, value: "c",}
6: Token {type: TokenType {label: "]",} }
7: Token {type: TokenType {label: "[",} }
8: Token {type: TokenType, value: "d",}
9: Token {type: TokenType {label: "]",} }
10: Token {type: TokenType {label: "eof",} }
]

从词法分析生成的 token 中就可以发现区别所在,[] 可能是个变量,从这点可以猜测出 a.b.c.d 更快。

更严禁的角度:字节码

如果仅仅从词法分析阶段生成的 token 去判断,可能还不是很准备,而且这是 Babel 的 parser,并不是 V8 的 parser。

可以从字节码去比较这两者的差异,结果会更准确些。

// cast.js
function test() {
const a = { b: { c: { d: 1 } } };
const demo1 = a.b.c.d;
const demo2 = a['b']['c']['d'];
}
test();

看下这段代码生成的字节码长什么样。

node --print-bytecode --print-bytecode-filter=test cast.js > bytecode.txt

生成的字节码整体非常长,借助于--print-bytecode-filter 指令可以得到想要的字节码段。

[generated bytecode for function: test]
Parameter count 1
Register count 4
Frame size 32
13 E> 0x1ed1ce91547e @ 0 : a5 StackCheck
30 S> 0x1ed1ce91547f @ 1 : 7d 00 00 08 CreateObjectLiteral [0], [0], #8
0x1ed1ce915483 @ 5 : 26 fb Star r0
73 S> 0x1ed1ce915485 @ 7 : 28 fb 01 01 LdaNamedProperty r0, [1], [1]
0x1ed1ce915489 @ 11 : 26 f8 Star r3
75 E> 0x1ed1ce91548b @ 13 : 28 f8 02 03 LdaNamedProperty r3, [2], [3]
0x1ed1ce91548f @ 17 : 26 f8 Star r3
77 E> 0x1ed1ce915491 @ 19 : 28 f8 03 05 LdaNamedProperty r3, [3], [5]
0x1ed1ce915495 @ 23 : 26 fa Star r1
97 S> 0x1ed1ce915497 @ 25 : 28 fb 01 01 LdaNamedProperty r0, [1], [1]
0x1ed1ce91549b @ 29 : 26 f8 Star r3
102 E> 0x1ed1ce91549d @ 31 : 28 f8 02 07 LdaNamedProperty r3, [2], [7]
0x1ed1ce9154a1 @ 35 : 26 f8 Star r3
107 E> 0x1ed1ce9154a3 @ 37 : 28 f8 03 09 LdaNamedProperty r3, [3], [9]
0x1ed1ce9154a7 @ 41 : 26 f9 Star r2
0x1ed1ce9154a9 @ 43 : 0d LdaUndefined
114 S> 0x1ed1ce9154aa @ 44 : a9 Return
Constant pool (size = 4)
Handler Table (size = 0)

分析字节码

You can think of V8's bytecodes as small building blocks that make up any JavaScript functionality when composed together. V8 has several hundred bytecodes. There are bytecodes for operators like Add or TypeOf, or for property loads like LdaNamedProperty. V8 also has some pretty specific bytecodes like CreateObjectLiteral or SuspendGenerator. The header file bytecodes.h defines the complete list of V8’s bytecodes.

可以把 V8 的字节码看作是小型的构建块,组合起来就构成了 JS 的功能。

每个字节码指定其输入和输出作为寄存器操作数。Ignition 使用寄存器 r0,r1,r2,... 和累加器寄存器。

CreateObjectLiteral

字面意思:创建对象字面量

Star r0

将值保存在某个寄存器或累加器中。

这边的意思:将这个对象字面量保存在 r0 寄存器中

LdaNamedProperty AccumulatorUse, OperandType, OperandType

可以在 V8 的头文件中找到 LdaNamedProperty 这个字节码。

V(LdaNamedProperty, AccumulatorUse::kWrite, OperandType::kReg, OperandType::kIdx, OperandType::kIdx)

LdaNamedProperty r0, [1], [1] 是在内存中查找名字为 1 的值,这里表示 a.b

LdaNamedProperty r0, [1], [1] // a.b
Star r3
LdaNamedProperty r3, [2], [3] // a.b.c
Star r3
LdaNamedProperty r3, [3], [5] // a.b.c.d
Star r1

那这段就表示 在 a 上查找 a.b.c.d 了。

LdaNamedProperty r0, [1], [1] // a['b']
Star r3
LdaNamedProperty r3, [2], [7] // a['b']['c']
Star r3
LdaNamedProperty r3, [3], [9] // a['b']['c']['d']
Star r2

同理这段是表示 a['b']['c']['d']

从字节码上可以看到这两种查找方式几乎没有任何区别。

简单地分析一下原因:V8 使用 HiddenClasses 来追踪对象模型;这里比较的是 JS 的上层查找方式,在 V8 中确实有不同的方式进行属性查找,这个例子中是一开始创建了对象字面量,后续并没有动态添加属性,所以两者都是使用 fast 模式进行查找。