【V8 v7.6】更快的 frozen & sealed 元素

简介

本文档内,描述了 Object.freeze(), Object.seal(), 和 Object.preventExtensions()

将数组在 字典模式(dictionary mode) 下使用,也因此导致他们访问速度变慢。

这里也将有针对这个问题的两种解决方案做展开说明。

Bug v8:6831

背景

对于内置的 Object.freeze(), Object.seal()Object.preventExtensions()ECMAScript5.1 说明书

中有详细的描述,在版本发布之后,这些接口就已经被广泛的使用。

目前,这些内置方法的使用致使 V8 中性能的消耗,因为他们都会将数组转变成 DICTIONARYELEMENTS 模式来使用,从而使得

访问这些数组将变得很慢。

标签模板(Tagger templates)

内部实现用到了 Object.freeze()

标签模板的内部对字符串的处理就是使用了 Object.freeze() 方法,以确保开发者不会混淆使用他们的值。

这些 stringsstrings.raw 数组在 tag 函数中都是有效的。

1
2
3
4
5
6
7
8
9
10
  function tag(strings, ...values) {
let s = strings[0];
for (let i = 1; i < strings.length; ++i) {
s += values[i - 1] + strings[i];
}
return s;
}

const res = tag`Hello ${41 + 1}!\n`;
console.log(res)

+RESULTS:

Hello 42!

之前在 《理解 es6》 就有提到 tag 标签的使用,其中有提到字符串原始值的问题 String.raw
即字符串中如果存在类似 \n 的转义字符,第一个参数数组上会有一个属性 strings.raw ,一个包含了未
转义的字符串序列。

使用费转义序列:

1
2
3
4
5
6
7
8
9
10
function tag(strings, ...values) {
let s = strings[0];
for (let i = 1; i < strings.length; ++i) {
s += values[i - 1] + strings.raw[i];
}
return s;
}

const res = tag`Hello ${41 + 1}!\n`;
console.log(res)

+RESULTS:

Hello 42!\n

标签模板被广泛应用到一些库

如今, Tagged templates 越来越被广泛的使用,并且越来越多的库和框架都加入了标签模板特性的使用。

例如:下面这些库就属于重度使用 tagged templates 的成员

  1. lit-html & LitElement
  2. styled-components
  3. htm
  4. common-tags

例如: lit-html 就允许你在 JavaScript 中书写 HTML 模板,然后高效的去将这些模板加上传入的数据,将他们渲染

成 DOM。

1
2
3
4
5
6
7
8
9
10
11
import {html, render} from 'lit-html';

// A lit-html template uses the `html` template tag:
let sayHello = (name) => html`<h1>Hello ${name}</h1>`;

// It's rendered with the `render()` function:
render(sayHello('World'), document.body);

// And re-renders only update the data that changed, without
// VDOM diffing!
render(sayHello('Everyone'), document.body);

例如,在 styled-components 中

1
2
3
4
5
6
7
8
9
10
11
12
13
const Button = styled.a`
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
margin: 0.5rem 1rem;
width: 11rem;
background: transparent;
color: white;
border: 2px solid white;
${props => props.primary && css`
background: white;
color: palevioletred;
`}`;

性能上的应变方案

JavaScript:

比如,其中的一个方案是使用 Babelloose mode ,它会忽略掉 Object.freeze() 的使用,而采用另一种简单的折中方案。

正常的 tagged templates 的实现,如 tag`Hello ${42}!\n`

1
2
3
4
5
6
7
8
9
10
var _templateObject = _taggedTemplateLiteral(
["Hello ", "!\n"],
["Hello ", "!\\n"]
);
function _taggedTemplateLiteral(strings, raw) {
return Object.freeze(
Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })
);
}
tag(_templateObject, 42)

但是由于 Object.freeze 性能上的问题,Babel 会在 loose mode 下将 tag 实现如下转变:

1
2
3
4
function _taggedTemplateLiteralLoose(strings, raw) {
strings.raw = raw;
return strings;
}

简单的对象属性置换。

TypeScript:

生成 JavaScript 的时候,默认就是生成无 frozen 版本的函数。

1
2
3
4
5
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
};
tag(__makeTemplateObject(["Hello ", "!\n"], ["Hello ", "!\\n"]), 42);

frozen对象和变形动画(orthogonal)

在 V8 中有一个跟 orthogonal 有关的问题,它也和 Object.freeze(), Object.seal(),
Object.preventExtensions() 的使用有关系。

性能影响对比

有两种使用场景需要考虑:

  1. latency 潜在危险
  2. throughput 服务端渲染的使用情况

strings 数组在“字典模式”下使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function tag(strings, ...values) {
let a = 0;
for (let i = 0; i < strings.length; ++i) a += strings[i].length;
return a;
}

function driver(n) {
let result = 0;
for (let i = 0; i < n; ++i) {
result += tag`${"Hello"} ${"cruel"} ${"slow"} ${"world"}!\n`;
result += tag`${"Why"} ${"is"} ${"this"} ${"so"} ${"damn"} ${"slow"}?!\n`;
}
return result;
}

driver(10);
driver(100);
driver(1000);
driver(10000);

console.time('Time');
driver(1e6);
console.timeEnd('Time');

+RESULTS:

Time: 205.946ms

Babel loose mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var _templateObject = _taggedTemplateLiteralLoose(
["", " ", " ", " ", "!\n"],
["", " ", " ", " ", "!\\n"]
),
_templateObject2 = _taggedTemplateLiteralLoose(
["", " ", " ", " ", " ", " ", "?!\n"],
["", " ", " ", " ", " ", " ", "?!\\n"]
);

function _taggedTemplateLiteralLoose(strings, raw) {
strings.raw = raw;
return strings;
}

function tag(strings) {
var a = 0;
for (var i = 0; i < strings.length; ++i) {
a += strings[i].length;
}
return a;
}

function driver(n) {
var result = 0;
for (var i = 0; i < n; ++i) {
result += tag(_templateObject, "Hello", "cruel", "slow", "world");
result += tag(_templateObject2, "Why", "is", "this", "so", "damn", "slow");
}
return result;
}

driver(10);
driver(100);
driver(1000);
driver(10000);

console.time("Time");
driver(1e6);
console.timeEnd("Time");

+RESULTS:

Time: 23.673ms
default(throughput) –noopt(latency)
内部实现 205.946ms 767ms
Babel loose mode 23.673 640ms

latency 模式的时间是原作者的结果时间。

throughput 的时间是上面两端代码在 Nodejs 环境下的实际运行时间。

从结果对比, loose mode / internal 相比 10 倍多的时间,这性能的影响还是很严重的。

本文标题:【V8 v7.6】更快的 frozen & sealed 元素

文章作者:ZhiCheng Lee

发布时间:2019年06月24日 - 10:30:36

最后更新:2019年06月28日 - 09:28:20

原始链接:http://blog.gcl666.com/2019/06/24/v8_array_fast_frozen_and_sealed/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%