第一,基本原理.
V8使用hidden classes连接transitions来发现蓬松不成形的JavaScript对象中的静态 struct .
隐藏类描述对象的 struct ,转换将隐藏类链接在一起,描述在对象上执行特定操作时应使用哪个隐藏类.
例如,下面的代码将导致以下隐藏类链:
var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;
此链是在构造o1
时创建的.构建o2
时,V8只需遵循已建立的转换.
现在,当属性fn
用于存储函数时,V8try 对该属性进行特殊处理:而不是仅仅在隐藏类中声明对象包含属性fn
V8 puts function into the hidden class.
var o = {};
o.fn = function fff() { };
现在这里有一个有趣的结果:如果将不同的函数存储到具有相同名称的字段中,V8将不能再简单地遵循转换,因为function属性的值与预期值不匹配:
var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };
当判断o2.fn = ...
赋值时,V8将看到有一个标记为fn
的转换,但它会导致一个不合适的隐藏类:它在fn
属性中包含fff
,而我们试图存储ggg
.注意:我给出函数名只是为了简单——V8在内部不使用它们的名称,而是使用它们的identity.
因为V8无法遵循这种转换,所以V8将决定将函数升级到隐藏类的决定是不正确和浪费的.情况将会改变
V8将创建一个新的隐藏类,其中fn
只是一个简单属性,不再是常量函数属性.它将重新路由转换,并标记旧的转换目标deprecated.记住o1
还在使用它.但是,下次代码触及o1
时,例如从中加载属性时,运行时将从不推荐的隐藏类中迁移o1
.这样做是为了减少多态性——我们不希望o1
和o2
有不同的隐藏类.
为什么在隐藏类上有函数很重要?因为这使V8的优化编译器信息达到inline method calls.如果调用目标存储在隐藏类本身上,它只能内联方法调用.
现在让我们把这些知识应用到上面的例子中.
因为在转换bar.fn
和foo.fn
之间存在冲突,所以转换bar.fn
和foo.fn
将成为普通属性——函数直接存储在这些对象上,V8无法内联foo.fn
的调用,从而导致性能降低.
它能在打电话之前接通吗?Yes.改变如下:在旧版本的V8 there was no deprecation mechanism中,即使在发生冲突并重新路由fn
转换之后,foo
也没有迁移到隐藏类中,其中fn
成为正常属性.相反,foo
仍然保留隐藏类,其中fn
是直接嵌入到隐藏类中的常量函数属性,允许优化编译器内联它.
如果在较旧的 node 上try 计时bar.fn
,您将看到它的速度较慢:
for (var i = 0; i < 100000000; i++) {
bar.fn(); // can't inline here
}
正是因为它使用了隐藏类,不允许优化编译器内联bar.fn
调用.
这里最后要注意的是,这个基准测试并没有衡量函数调用的性能,而是衡量优化编译器是否可以通过内联调用将这个循环减少为空循环.