深度继承链是否会减慢 V8 JavaScript 引擎中的方法查找速度?
Is a deep inheritance chain slowing down method lookup in V8 JavaScript engine?
我正在用 TypeScript 为游戏编写基础 class。它具有发送消息、资源管理等功能。
受Mixins的启发,我写了如下代码(编译为JavaScript):
function Messenger(Base) {
return class Messenger extends Base {
$dispatch(e) {
// TODO
}
};
}
function ResourceManager(Base) {
return class ResourceManager extends Base {
$loadRes(key) {
// TODO
return Promise.resolve({});
}
};
}
class Component {
}
class GameBase extends Component {
start() {
console.log('start');
}
init() {
console.log('init');
}
}
const Klass = ResourceManager(Messenger(GameBase));
var gg = new Klass();
gg.start();
据我所知,当我尝试调用 gg.start
时,JavaScript 引擎查找原型链,在这种情况下它会稍微长一些,当混合增长:
这会减慢方法查找速度吗?
V8 是否优化了这个查找过程,我可以忽略查找开销吗?
我写了一个小基准测试来查看沿着原型链的查找会花费多少(小心,当点击 'Run code snippet' 时它会阻塞你的浏览器;而是在 Node 本地执行它):
function generateObjectWithPrototype(prototype) {
const f = function() {};
f.prototype = prototype;
return new f();
}
const originalObject = new (function() {
this.doSomething = function() {};
})();
let currentObject = originalObject;
for (let i = 0; i < 60001; i++) {
currentObject = generateObjectWithPrototype(currentObject);
const start = +new Date();
currentObject.doSomething();
const end = +new Date();
if (i % 10000 === 0) {
console.log(`Iteration ${i}: Took ${end - start}ms`);
}
}
结果:
Iteration 0: Took 0ms
Iteration 10000: Took 0ms
Iteration 20000: Took 1ms
Iteration 30000: Took 1ms
Iteration 40000: Took 2ms
Iteration 50000: Took 3ms
Iteration 60000: Took 4ms
所以在这种情况下,对于 60,000 的 prototype-depth,找到 doSomething()
方法所需的额外时间大约为 4 毫秒。我会说这是可以忽略不计的。
这里是 V8 开发人员。这是一个复杂的问题;简短的回答是 "it depends".
在进行查找时必须走更长的原型链需要更多时间,这是微不足道的事实。但是,如果只完成一次或两次,那么时间通常太短而无足轻重。
所以下一个问题是:将多久执行一次此类查找? V8 会尽可能地缓存查找结果(如果您想了解更多,请搜索术语 "inline caches");与所有缓存一样,此类缓存的有效性关键取决于看到的不同案例的数量。
因此,如果您的代码主要是 "monomorphic"(即在任何给定的 foo.bar
查找中,foo
将始终具有相同的 type/shape,包括相同的原型链),或 low-degree 多态(最多四种不同类型的 foo
),那么完整的原型链遍历只需要完成一次(或分别最多四次),之后缓存的结果将被使用,所以如果你执行这样的代码数千次,你不会看到一步或几百步长的原型链之间的性能差异。
另一方面,如果您有 属性 加载或存储看到许多不同的类型(在某些框架中往往会发生这种情况,其中每次查找都会通过一些中央 getProperty(object, property) { /* do some framework stuff, and then: */ return object[property]; }
函数) ,那么缓存就没用了,V8 每次都要进行全查找。这对于长原型链来说特别慢,但是这意味着它总是比可缓存的情况慢得多(即使是短原型链)。
总而言之,如果您对整体程序设计比较谨慎并且避免在相同的代码位置有许多不同的类型,那么您可以轻松地承受很长的原型链。事实上,尽可能多地保持代码的单态类型往往比保持原型链长度短有更大的影响。另一方面,较短的原型链长度确实让引擎的寿命更轻松,我个人认为它们 can(如果你不过分)也提高了可读性,所以所有在其他条件相同的情况下,我建议您尽可能保持对象模型简单。
我正在用 TypeScript 为游戏编写基础 class。它具有发送消息、资源管理等功能。 受Mixins的启发,我写了如下代码(编译为JavaScript):
function Messenger(Base) {
return class Messenger extends Base {
$dispatch(e) {
// TODO
}
};
}
function ResourceManager(Base) {
return class ResourceManager extends Base {
$loadRes(key) {
// TODO
return Promise.resolve({});
}
};
}
class Component {
}
class GameBase extends Component {
start() {
console.log('start');
}
init() {
console.log('init');
}
}
const Klass = ResourceManager(Messenger(GameBase));
var gg = new Klass();
gg.start();
据我所知,当我尝试调用 gg.start
时,JavaScript 引擎查找原型链,在这种情况下它会稍微长一些,当混合增长:
这会减慢方法查找速度吗? V8 是否优化了这个查找过程,我可以忽略查找开销吗?
我写了一个小基准测试来查看沿着原型链的查找会花费多少(小心,当点击 'Run code snippet' 时它会阻塞你的浏览器;而是在 Node 本地执行它):
function generateObjectWithPrototype(prototype) {
const f = function() {};
f.prototype = prototype;
return new f();
}
const originalObject = new (function() {
this.doSomething = function() {};
})();
let currentObject = originalObject;
for (let i = 0; i < 60001; i++) {
currentObject = generateObjectWithPrototype(currentObject);
const start = +new Date();
currentObject.doSomething();
const end = +new Date();
if (i % 10000 === 0) {
console.log(`Iteration ${i}: Took ${end - start}ms`);
}
}
结果:
Iteration 0: Took 0ms
Iteration 10000: Took 0ms
Iteration 20000: Took 1ms
Iteration 30000: Took 1ms
Iteration 40000: Took 2ms
Iteration 50000: Took 3ms
Iteration 60000: Took 4ms
所以在这种情况下,对于 60,000 的 prototype-depth,找到 doSomething()
方法所需的额外时间大约为 4 毫秒。我会说这是可以忽略不计的。
这里是 V8 开发人员。这是一个复杂的问题;简短的回答是 "it depends".
在进行查找时必须走更长的原型链需要更多时间,这是微不足道的事实。但是,如果只完成一次或两次,那么时间通常太短而无足轻重。
所以下一个问题是:将多久执行一次此类查找? V8 会尽可能地缓存查找结果(如果您想了解更多,请搜索术语 "inline caches");与所有缓存一样,此类缓存的有效性关键取决于看到的不同案例的数量。
因此,如果您的代码主要是 "monomorphic"(即在任何给定的 foo.bar
查找中,foo
将始终具有相同的 type/shape,包括相同的原型链),或 low-degree 多态(最多四种不同类型的 foo
),那么完整的原型链遍历只需要完成一次(或分别最多四次),之后缓存的结果将被使用,所以如果你执行这样的代码数千次,你不会看到一步或几百步长的原型链之间的性能差异。
另一方面,如果您有 属性 加载或存储看到许多不同的类型(在某些框架中往往会发生这种情况,其中每次查找都会通过一些中央 getProperty(object, property) { /* do some framework stuff, and then: */ return object[property]; }
函数) ,那么缓存就没用了,V8 每次都要进行全查找。这对于长原型链来说特别慢,但是这意味着它总是比可缓存的情况慢得多(即使是短原型链)。
总而言之,如果您对整体程序设计比较谨慎并且避免在相同的代码位置有许多不同的类型,那么您可以轻松地承受很长的原型链。事实上,尽可能多地保持代码的单态类型往往比保持原型链长度短有更大的影响。另一方面,较短的原型链长度确实让引擎的寿命更轻松,我个人认为它们 can(如果你不过分)也提高了可读性,所以所有在其他条件相同的情况下,我建议您尽可能保持对象模型简单。