V8引擎简介

转载自: http://impd.tencent.com/?p=35

于上面的那篇英文文章对比着看效果更好。

 

V8引擎简介

QQ2013中使用Webkit内核替换原IE内核,内核中使用V8作为JS引擎,使得JS执行性能有了极大的提升,本文主要分析了一下V8引擎的实现与优势。

 

V8引擎作为一种动态语言运行时平台,需要实现动态语言源程序解析、执行,基本流程如下:

其中各部分非固定不变,主要有以下模式:

  1. 虚拟机模式:编译成AST或字节码后,执行环境提供AST或字节码的执行。
  2. 本地代码模式:直接把字节码翻译成机器码,由CPU进行执行,类似静态语言。
  3. 混合模式:部分代码翻译成机器码,同时需运行时提供一些语言级别的动态访问能力。

其中虚拟机模式较为常用,实现简单且移于移植,但执行速度较慢;本地代码模式执行速度较快,对动态语言的支持复杂度较大;混合模式介于两者之间,将常用代码编译成机器码以提高执行速度。

用于提供动态语言特性支持的运行时环境,主要需要实现以下内部分内容:

  1. 对象模型,用于实现语言级别的泛型支持。
  2. 变量存储、访问、传递、回收。
  3. 调度控制,实现模拟CPU实现的语句调度等。

二.V8的设计

2.1 对象模型

对于JavaScript这类动态语言,不同于静态语言的确定性,动态语言中对象在运行时不仅类型可变,属性也可改变、增减:

var m = new object();
m=123;//先是一个数字
m[“a”]=234;//然后是一个map
m[1]=”abc”;
m[“2″][“b”]=”abc”;//然后是一个tree

故运行时环境需提供动态检测对象类型的功能,同时可动态增删、访问属性或数组元素(对应于数据结构中的map与array)。

先来看两种经典的泛型实现:

(1)全托管类型

全托管类型将对象分为类型与值两部分,值中托管了所有可能的对象类型。

以下是腾讯广研公共组件TData的实现:

classTData
{
TEDataTypeemDataType;//数据类型
void* pData;//指向数据的万能指针
}

其中pData为托管的指针,指向存储的值,托管了所有基本类型以及数据结构,其中Array的实现与map相同:

TData托管的类型 实际的存储类型
布尔值 bool
浮点数 double
整数 int64
字符串型 std::string
Map std::map<string,TData *>
Array 同上
NULL

(2) 部分托管

部分托管模型将值与属性、数组元素分离,简单模型如下:

typedefunion
{
void* p;
int n;
} Value;

typedefstruct
{
int t;
Value v;
map<string, TObject*> properties;  //访问属性
TObject** elements;           //访问array元素
} TObject;

其中Value是个可扩展union,如lua中对应实现如下:

typedefunion
{
GCObject* gc;
void* p;
lua_Number n;
intb;
} Value;

若使用map来实现数组,则elements与properties可合并。

全托管类型全部使用堆分配,实现较为简单,但需分配两次内存;部分托管类型部分变量在栈分配(实际上还是堆),访问int等基本类型较快,同时明确区分值与属性、元素,可分别进行优化。

V8引擎的实现类似于部分托管类型,但接管了内存分配与管理,除了Smi(Small Integer),其他对象均在堆上分配。V8中所有对象均使用4字节(32位机)表示(伪码,非实际实现

typedef union
{
int32SmiValue;
void*p;
} Object;

32位内存的末位为0时表示Smi,为1时表示指针,从而区分Smi与堆上分配的对象。整体继承关系如下图(仅列出部分对象):

所有JS对象继承自JSObject,JS函数继承自JSProxy。

JSObject的内存分布如下:

MapPointer(32 bits) PropertiesPointer(32 bits) ElementsPointer(32 bits)

其中MapPointer继承自HeapObject,指明对象类型、占用内存大小以及GC需要的一些信息,PropertiesPointer对应于属性访问的map,ElementsPointer对应于Array中的元素访问(即key为整数的属性访问)。可以看出JSObject的实现与部分托管相同,但不同于其中map与array的实现,V8中对属性及元素的访问进行了优化,见下节分析。

2.2 快速属性访问

JavaScript中不明确区分数组元素与属性,数组元素可看做以整数为key的map,v8中把两者分开处理,以提高array的访问速度。

(1)属性访问

先来看段代码:

function Point(x, y)
{
this.x = x;
this.y = y;
}

var point = new Point(2,3);
point.x = 4;
point.y = 5;

对于C++等静态语言,以上后两行代码可看成:

set [point+xOffset] 5
set [point+yOffset] 5

其中xOffset与yOffset为预先计算出来的常量,属性操作只需直接访问对应内存。对于JS这种动态语言无法实现这样的操作,因为创建对象后属性可动态增减,不可能预先计算出属性对应偏移值。

因此,大部分动态语言运行时的设计均是采用两种方式实现:

  1. map实现:以属性为key,红黑树查找,复杂度为O(logN)
  2. HashTable实现:通过属性计算hash值,复杂度为O(1)

显然HashTable复杂度优于map,但根据字符串计算hash值效率不高,且需要处理冲突问题,实际并非最优。以上两方式的实现同时需要在每个对象中存放各个属性名称,内存占用上也存在较大优化空间。

V8中针对属性的访问类似于HashTable的实现,但特别进行了以下优化:

1. 编译期,相同的属性名称字符串存放于堆上相同位置,将string转化为地址。

2. 引入隐藏类存放该类型中各属性的相对偏移值,针对属性个数的多少分别采用二分查找与线性查找的方式获取偏移值。

3. 引入以隐藏类+属性为key, 属性偏移值为value的Cache,提高重复访问属性时响应速度。

采用隐藏类实现快速访问,对象模型可简化表示为:

classTypeObject
{
int type;
intslotCount;
HashMap*slotMap;
};

class Object
{
TypeObject* map;
Object* properties;
Object* elements;
}

其中map中的slotMap即V8官网上所指的隐藏类,指明了每个属性对应的偏移值,properties与elements分别存放以string为key与以int为key的属性,可看成数组。如对以上的Point类,隐藏类如下:

以上实现模拟静态语言,虽然编译时对象没有类信息,但运行时动态生成类信息(称为隐藏类),隐藏类中存放各属性偏移值,且具有相同结构的对象指向相同隐藏类(隐藏类的实现详见官网https://developers.google.com/v8/design)。

对于访问对象属性的以下语句:

point.x=5

实际访问过程可简化为:

int index = SearchCache(point->slotMap, x);

//搜索Cache
if(index != NotFind)
{
SetProperty(point, index, 5);
return;
}

//搜索隐藏类
index = point->slotMap.Search(hash);
if(index != NotFind)
{
SetProperty(point, index, 5);
return;
}

//更新隐藏类
UpdateHiddenClass(point->slotMap, x);
index = point->NextFreePropertyIndex();
SetProperty(point, index, 5);

//更新Cache
UpdateCache(point->slotMap, x, index);

V8编译JS时将字符串”x”转化为对应StringObject,因此后续计算hash的操作仅需操作32位整数,同时使用了内联Cache及隐藏类确保属性的快速访问。

(2) array访问

array访问相对简单些,当出现过的最大下标较小且空间足够时,采用数组形式实现,否则采用HashTable方式实现。采用数组形式时,下标直接为数组下标,可直接访问,采用HashTable方式时,用下标计算Hash值,需要增加个求模操作,两者复杂度均为O(1)。对属性、数组元素访问源代码如下:

var array = new Object();
array.prop = 2; //属性方式,设置在properties中
array[3] = 3; //数组方式,设置在elements中
array[3000]=4;//原数组转化为HashTable,以下标为key,设置在elements中

综上,V8中对属性、数组元素访问流程如下:

2.3 变量管理

变量管理是运行时环境开发的难点,其中涉及变量访问、作用域控制、生命期管理三大部分内容。

传统运行时采用map保存变量,采用作用域链的方式控制变量访问及生命期管理,但对于引用类型的生命期管理无法完美解决,因此引入GC进行生命期控制。

V8中除Smi所有变量均在堆上分配,使用32bit的地址表示变量,由GC控制变量生命期。每个作用域中申请一个HandleScope,后续申请的变量均使用一个Handle持有该变量,并保存Handle到HandleScope中,当超出HandleScope作用域时,销毁HandleScope并释放其中所有Handle,由GC根据堆上变量是否被引用来决定变量的回收。

回收算法为分代回收,新申请的小对象放入Young Generational,未被回收对象放入Old Generational,针对不同时机分别采用Scavenge、Full non-compacting collection、Full compacting collection三种算法进行垃圾回收。

2.4 调度控制

基于虚拟机模式的调度控制通过模拟CPU的方式实现,包括取指令、目标跳转、参数压栈等,通常采用一个大switch实现字节码的执行与目标跳转。

而V8是采用动态代码生成,直接将树状的AST编译成线形的机器码,由CPU直接执行,因此省去指令执行与调度方面难点,并提高执行速度。

. V8为什么快

相对于别的JS引擎,V8中主要引入下面几方面的优化以提高执行速度:

1. 编译优化:实现string转32bit地址,减小关于string的hash计算或搜索时间。

2. 属性访问优化:与常规hash方法相同,但引入隐藏类归纳相同结构减小内存占用。同时采用隐藏类-属性的内存Cache,实现属性快速访问。

3. 执行优化:采用动态代码生成,执行时减小字节码解析与执行的时间。

4. Cache优化:对代码、属性访问等引入Cache,提高重复执行代码的执行效率。

5. 垃圾回收机制:统一管理内存,减小作用域切换时频繁的内存分配与释放开销。

四. 总结

V8引擎采用的优化特性大大提高JS代码循环语句、属性访问、重复调用等方面的执行速度。

相对于原IE内核,V8引擎在JS执行速度上有极大提升,以下为各项JS执行性能测试的对比结果:

目前V8引擎的缺点是内存占用较大,需要保留各类Cache空间,GC机制也会预留约16M的空间,还需要进行优化。

本条目发布于2013年5月10日。属于未分类分类。作者是店小二

你可能感兴趣的:(js,V8)