V8知识总结

摘录自http://newhtml.net/v8-object-representation/

V8中对32bits长的值做了进一步分类,其中最低位作为区分,如果为0则表示该值为31bits长的整数;如果为1则表示该值为30bits长的指针。由于V8中的对象以4Bytes为单位对齐,指针的最低2位恰好空闲。


object-representation.png

所有的对象内存区都会有一个Map指针,用以描述该对象的结构。绝大多数对象将其自身的属性存放在一块内存中(“a”和“b”);附加的命名属性通常会存放在一个单独的数组中(“c”和“d”);而数字式的属性则单独存放在另一个地方,通常是一个连续的数组。

属性的怪异属性

你可以以两种方式来访问对象的属性:

obj.prop
obj["prop"]

根据标准,属性的名称永远是字符串。如果你用不是字符串的东西来作为属性的名称,那它将会被隐式转换为字符串。所以一个怪异的情况就是,如果用数字作为属性名,则数字也会被转换为字符串(至少根据标准就是这样)。因此,你可以以小数或者负数来作为下标。

obj[1];    //
obj["1"];  // 这些都是同一个属性哦
obj[1.0];  //

var o = {toString: function () { return "-1.5"; } };
obj[-1.5]; // 这俩也是同一个属性
obj[o];    // 因为o转换成了字符串

数组在JS中也只是带有神奇length属性的对象。大多数数组的属性名都是非负整数,而length的值则来计算于这些属性名中最大的那个加一,比如:

var a = new Array();
a[100] = "foo";
a.length;             // 返回101

字典模式

也即哈希表模式
V8内部实际上也用了这样的方式来表达一些难以用优化形式表达的对象。但访问哈希表中的值要比访问指定偏移的值慢多了。

0:  map (字符串类型)
4:  length (字符数)
8:  hash code (惰性计算而来)
12: characters...

左边是偏移量,右边是该偏移量起始内存存放的值含义;从0开始,除最后一处外每个要素占用4字节,最后一处则是长度为length的字符
字符串通常不可变,唯一可能变的是惰性计算而来的哈希值。用做属性名的字符串被称为符号,这意味着它必须独有(译注:原文uniquified,意思是这个字符串对象不会因为在其他地方也引用了,导致其它地方可以对这个对象的内部进行修改),非独有的字符串如果被用作属性名,都会被单独复制一份出来,以便不受其它修改的影响。
V8中的哈希表由一个包含键和值的大数组组成。初始时,所有的键和值都被初始化为undefined(一个特殊值),当有键值对插入到哈希表中时,键的哈希值被计算出来,其低位被用作数组的下标。如果数组的该位置已经被占用,则哈希表尝试(取模过后的)下一个位置,以此类推。

快速的对象内属性

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

绝大多数时间里,同一构造函数所产生的对象会拥有以相同顺序赋值的相同属性。既然这些对象有着如此类似的结构,我们在内存中就可以以这样相同的结构来布局这些对象。
V8将这种描述对象的方式称为Map。你可以假想Map为一张填满描述符的表,每一项都表示一个属性。Map也包含其他信息,比如对象的大小以及指向构造函数和原型的指针等。同样结构的对象,通常会共享同一个Map。一个完成初始化的Point实例可能就像这样:

Map M2
        object size: 20 (2个属性的空间)
        "x": FIELD at offset 12
        "y": FIELD at offset 16

现在你可能会想到,不是所有的Point实例都有相同的属性。当Point的实例刚刚在内存中开辟空间时(在构造函数中的代码真正执行前),它是没有任何属性的,Map M2并不符合它的结构。另外,我们也可以在构造函数完成后随时为它增加新的其他属性。

V8处理通过一种特殊的描述符来处理这种情形:Transition。当增加一个新的属性时,除非迫不得已,我们不会创建新的Map,而是尽可能使用一个现存符合结构的Map。Transition描述符就是用来指向这些Map的。

Map M0
        "x": TRANSITION to M1 at offset 12

    this.x = x;

    Map M1
        "x": FIELD at offset 12
        "y": TRANSITION to M2 at offset 16

    this.y = y;

    Map M2
        "x": FIELD at offset 12
        "y": FIELD at offset 16

在上面的例子中新的Point实例从没有任何Field的M0开始;在第一次赋值时,对象的Map指针指向了M1,属性x的值存放在了偏移量12的位置;在第二次赋值时,Map指针指向了M2,属性y的值放在了偏移量16的位置。
如果在M2的基础上再新增属性呢?

Map M2
        "x": FIELD at offset 12
        "y": FIELD at offset 16
        "z": TRANSITION to M3 at offset 20

    this.z = z;

    Map M3
        "x": FIELD at offset 12
        "y": FIELD at offset 16
        "z": FIELD at offset 20

如果新增的属性之前没有,则我们会通过复制M2创建一个新的Map,M3,然后将一个新的FIELD描述符增加在M3上。同时我们要在M2上增加一个TRANSITION描述符。注意,新增TRANSITION是修改Map为数不多的情况之一,通常Map是不可变的。

如果对象的属性并不是以相同的顺序出现呢?比如:

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

在这种情况下,我们最终会得到一个Transition树,而不是链。初始的Map(上面的M0)将会有两个Transition,具体代码中转向哪个,会根据x和y的赋值顺序来定。正因为这样,不是所有的Point都会有相同的Map了。
这正是事情变糟的地方。V8对于这样的小规模分支情形可以容忍,但如果你的代码中充斥着以同一个构造函数得出的随机赋值对象,V8就会将其退化到字典模式,将属性存放在哈希表中。否则就会有大量的Map涌现。

成员函数与原型

JavaScript没有类,因此它的成员函数调用与C++及Java不同。JavaScript中的成员函数只是普通的属性。在下面的例子中,distance只是Point对象的一个属性,它指向PointDistance函数。JavaScript中的任何函数都可以作为成员函数,并且通过this来访问其目标对象。

译注:在C++中,obj.method(param)实际是C代码method(this, param)的语法糖,因此this指针实际是函数的目标对象,而不是函数的发起者。

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

    function PointDistance(p) {
        var dx = this.x - p.x;
        var dy = this.y - p.y;
        return Math.sqrt(dx*dx, dy*dy);
    }

数字式属性:Fast Element

至此,我们已经讨论了普通属性和方法,并且假设对象总是以相同顺序构造相同的属性。但这对于数字式的属性(以下标的形式来访问的数组元素)并不成立,同时任何对象都有可能像数组一样使用,因此我们需要对数组一样的对象区别对待。记住,根据标准,所有的属性都必须是字符串,其他类型会先转换为字符串。
我们将属性名为非负整数(0、1、2……)的属性称为Element。V8中,Element的存放和其他属性是分开的。每个对象都有一个指向Element数组的指针,对象Map中的Element Field将反映出Element是如何存储的。注意,Map中并不包含Element的描述符,但可能包含其它有着不同Element类型的同一种Map的Transition描述符(译注:换言之,一个Map只对应一种Element数组,如果Element数组的类型不同,会形成一个Transition。)。大多数情况下,对象都会有Fast Element,也就是说这些Element以连续数组的形式存放。有三种不同的Fast Element:

- Fast small integers
- Fast doubles
- Fast values

根据标准,JS中的所有数字都理应以64位浮点数形式出现,尽管我们平时处理的都是整数。因此V8尽可能以31位带符号整数来表达数字(最低位总是0,这有助于垃圾回收器区分数字和指针)。因此含有Fast small integers类型的对象,其Element类型只会包含这样的数字。如果需要存储小数、大整数或其他特殊值,如-0,则需要将数组提升为Fast doubles。于是这引入了潜在的昂贵的复制-转换操作,但通常不会频繁发生。Fast doubles仍然是很快的,因为所有的数字都是无封箱存储的。但如果我们要存储的是其他类型,比如字符串或者对象,则必须将其提升为普通的Fast Element数组。
JavaScript不提供任何确定存储元素多少的办法。你可能会说像这样的办法,new Array(100),但实际上这仅仅针对Array构造函数有用。如果你将值存在一个不存在的下标上,V8会重新开辟更大的内存,将原有元素复制到新内存。V8可以处理带空洞的数组,也就是只有某些下标是存有元素,而期间的下标都是空的。其内部会安插特殊的哨兵值,因此试图访问未赋值的下标,会得到undefined。
当然,Fast Element也有其限制。如果你在远远超过当前数组大小的下标赋值,V8会将数组转换为字典模式,将值以哈希表的形式存储。这对于稀疏数组来说很有用,但性能上肯定打了折扣,无论是从转换这一过程来说,还是从之后的访问来说。如果你需要复制整个数组,不要逆向复制(索引从高到低),因为这几乎必然触发字典模式。

// 这会大大降低大数组的性能
    function copy(a) {
        var b = new Array();
        for (var i = a.length - 1; i >= 0; i--)
            b[i] = a[i];
        return b;
    }

由于普通的属性和数字式属性分开存放,即使数组退化为字典模式,也不会影响到其他属性的访问速度(反之亦然)。

你可能感兴趣的:(V8知识总结)