v8 上的 JS 性能优化

这次到北京的主要目的,主要在于参加由 JSConf 组织主办的《京JS》会议,去年同期该组织也曾经举办过《沪JS》,会议主要内容为 《前端js的相关优化》 以及 《nodejs服务器端的相关技术分享》。

这次会议的收获主要归结为以下几点:

  1. 一场专注于 JavaScript 和 Node.js 的技术会议。
  2. 开发者对 nodejs 的使用频率成大幅上升。
  3. 各大公司对 nodejs 的投入力度加大。
  4. 了解到国外业界牛人对 web 及 nodejs 开发的理解,及踩过的坑。

下面主要分享一下个人感兴趣的几个主题。

百度 云/端一体化框架 Clouda

客户端/服务器端通过 Clouda 框架实现数据模型打通。即一个 Model 前后通用。再也不用担心我做把数据序列化,然后在后端反序列化这样重复的苦力活了。同时 subpub 机制也实现了云、端数据的准实时同步。后端的变化可以准实时地反应到前端呈现。MVVM 实现了视图和数据模型的双向绑定。而 Clouda 实现了客户端和服务端数据的双向绑定。

阿里 多浏览器前端测试服务 totorojs

一套基于网络的多浏览器管理服务,实现一键运行,多浏览器同时测试,汇总生成报表的功能。由于基于网络,IE6/7/8/9/10/11 等,甚至包括手机端,都可以在自己的机器、自己的系统下运行,最后由该服务统一管理和调度。

网易 实时游戏框架 pomelo

基于 Node.js 的高性能、分布式游戏服务器框架。包括基础的开发框架和相关的扩展组件(库和工具包),可以帮助你省去游戏开发枯燥中的重复劳动和底层逻辑的开发。Pomelo 不但适用于游戏服务器开发, 也可用于开发高实时 Web 应用,它的分布式架构可以使 Pomelo 比普通的实时 Web 框架扩展性更好。

Firefox OS

WEB APP API 提供强大的本地操作能力。以Gecko浏览器引擎为核心,采用HTML5来开发。所有应用都基于网络,希望用户一开机就能够连接上互联网,但也可通过HTML5 cache Manifest或相关API在脱机时使用

Google AngularJS

这是这次北京之行的意外收获,AngularJS 号称是下一代 web 应用的开发框架,在会议中也有不少讲师提到这个框架,现场也有出售关于这个框架的书籍。不过可惜,现场没有买到,在第一天上午就卖完了。后来经过多方了解,这个框架确实有其内在的美,值得大家参考学习。

V8 性能解决方案

这是我个人最最感兴趣的一个会议主题,演讲者为 Opera 的 Sphinx 团队的核心成员,虽然演讲能力不是有待提高,但其谈到的内容,确实改变了我对 JS 优化的思考角度。下面将结合我自己的理解,剖析一下关于JS在V8上的优化策略。

1.隐藏类

1
2
3
4
5
6
7
8
function test(x) {
this.x = x;
}
console.time("time1");
for (var i = 0; i < 100000000; ++i) {
var obj = new test(i);
}
console.timeEnd("time1");

1
2
3
4
5
6
7
8
9
function test1(x) {
this[x] = x;
}

console.time("time2");
for (var i = 0; i < 100000000; ++i) {
var obj = new test1(i);
}
console.timeEnd("time2");

以上2段代码之所以在速度上有天壤之别,其原因就在于V8的属性快速查找方式–隐藏类机制所导致的。每当有对象在 V8 上创建时,V8 会为该对象动态生成隐藏类,每当有新属性添加到该对象时隐藏类会对应改变,隐藏类可重用。

当 new Point(x, y) 执行的时候,一个新的 Point 对象会被创建出来。如果这是 Point对象第一次被创建,V8 会为它初始化一个隐藏类,不妨称作 C0。因为这个对象还没有定义任何属性,所以这个初始类是一个空类。到这个时候为止,对象 Point 的隐藏类是 C0。

执行函数 Point 中的第一条语句(this.x = x;)会为对象 Point 创建一个新的属性x。此时,V8 会:
在 C0 的基础上创建另一个隐藏类 C1,并将属性 x 的信息添加到 C1 中:这个属性的值会被存储在距 Point 对象的偏移量为 0 的地方。
在 C0 中添加适当的类转移信息,使得当有另外的以其为隐藏类的对象在添加了属性 x之后能够找到 C1 作为新的隐藏类。此时对象 Point 的隐藏类被更新为 C1。

优化策略:
1.在构造函数中初始化所有属性
2.始终按照同样顺序初始化属性
3.不要加载未初始化或已删除的元素

2.内联缓存,JIT 编译

1
2
3
4
5
6
7
8
9
function add(x, y) { 
return x + y;
}
console.time("time1");
for (var i = 0; i < 90000000; ++i) {
add('a','b');
add(1, 2);
}
console.timeEnd("time1");
1
2
3
4
5
6
7
8
9
10
11
12
function add1(x, y) { 
return x + y;
}

console.time("time2");
for (var i = 0; i < 90000000; ++i) {
add1('a', 'b');
}
for (var i = 0; i < 90000000; ++i) {
add1(1, 2);
}
console.timeEnd("time2");

JIT 编译(Just-in-time Compile)

   传统编译流程:
       代码段 -> 语法树 -> 中间码 -> 机器语言

   v8编译流程:
       代码段 -> 语法树 -> 机器语言

   没有解释器,属性访问通过内联缓存代码完成。

内联缓存(Inline Cache)

在第一次执行到访问某个对象的属性的代码时,V8 会找出对象当前的隐藏类。同时,V8 会假设在相同代码段里的其他所有对象的属性访问都由这个隐藏类进行描述,并修改相应的内联代码让他们直接使用这个隐藏类。当 V8 预测正确的时候,属性值的存取仅需一条指令即可完成。如果预测失败了,V8 会再次修改内联代码并移除刚才加入的内联优化。

 实例代码中,例子一命中 0 次,例子二命中失败 2 次。

优化策略:
1.不要使函数体过大
2.使方法职能单一,即确保属性变量只使用相同隐藏类包含的对象

3.数组模式(字典模式/快速模式)

快速模式:对应C语言的数组,速度快,紧凑
字典模式:对应C语言的哈希表,速度慢,松散

1.如果数组索引从 0 到 length-1,且无空洞,进入快速模式。
2.当预分配数组大于等于 100000,且数组有空洞,进入字典模式。
3.当预分配数组小于 100000,无论有无空洞,数组进入快速模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var t = new Array(100000); 

console.time('time1');
for (var i = 0; i < 100000; ++i) {
t.push(1);
}
console.timeEnd('time1');

var t = [];

console.time('time2');
for (var i = 0; i < 100000; ++i) {
t.push(1);
}
console.timeEnd('time2');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var t = new Array(99999);

console.time("time1");
for (var i = 0; i < 99999; ++i) {
t.push(1);
}
console.timeEnd("time1");

var t = new Array(99999 + 1);

console.time("time2");
for (var i = 0; i < 99999; ++i) {
t.push(1);
}
console.timeEnd("time2");

优化策略:
1.从0开始连续的初始化数组,以避免数组进入字典模式
2.不要预分配一个超大数组(比如长度大于等于 100000)
3.不要删除数组中的元素,尤其是数字数组。同样会转换为字典模式
4.不要访问未初始化或已删除的数组元素

偶遇疯狂猜图CTO:

第一天的午饭是一个偶然的机会,和《疯狂猜图》的 CTO 一起吃了顿午饭,交流了一下一些游戏相关的开发心得,感觉在中国游戏的成功并不在于技术有多牛,更多的是想法有多特别,其实在IT的世界,技术从是天外有天,我们要抓紧的往往是当下这一刻的想法,哪怕仅仅是一个闪念。

BYVoid:

出发到北京的前几天,正好关注到清华牛人郭家宝(BYVoid),这次《京JS》会议偶然发现了他也在现场有点惊讶,这里就带过一下了,哈!