为什么重新分配函数指针会减慢函数调用速度



我一直在使用函数指针p来调用不同的函数。我的问题是,如果在已经为其分配了函数后将其指向其他函数,则调用p的性能会大大降低。如果我一遍又一遍地在undefined和同一函数之间切换p,性能很好,当我只将其指向一个函数时,性能很好,但在函数之间切换会扼杀性能。

下面是我用来测试此场景的代码,这是小提琴。我递归循环 500 次,每次循环调用p1,000,000 次。p可以是undefined或指向func1func2

function func1() {} // two identical empty functions
function func2() {} // different in name only
var p = func1; // default to func1
var count  = 0; // current loop
var elapse = 0; // elapsed time for 1,000,000 calls on each loop
var start  = 0; // start time for 1,000,000 calls on each loop
var total  = 0; // total elapsed time for all loops
function loop() {
start = performance.now(); // get start time
for (let i = 0; i < 1000000; i ++) if (p !== undefined) p(); // do 1,000,000 calls or early out 1,000,000 times if undefined
elapse = performance.now() - start;
total += elapse; // used for getting average
count ++;
console.log(p + "nelapsed " + elapse + "naverage " + total / count);
// Switch between the lines below to see the performance difference.
p = (p === func1) ? p = undefined : p = func1; // best performance
//p = (p === func1) ? p = func1 : p = func1; // good performance
//p = (p === func1) ? p = func2 : p = func1; // bad performance
// pattern: func1 -> undefined -> func2 -> undefined -> repeat
/*if (p === undefined) p = (count % 4 === 0) ? p = func1 : p = func2;
else p = undefined;*/ // also bad performance
if (count < 500) loop(); // start the next loop
}
console.clear();
loop(); // start the loop

为什么调用p在分配不同的函数时性能会显著下降?另外,为什么将p设置为undefined并返回到原始函数时,将p设置为undefined然后设置为其他函数时不会改变性能?

您正在阻止引擎创建优化的热路径,因为它不能依赖于函数指针的值。

请参阅本文中标题为"JavaScript 引擎中的解释器/编译器管道"的部分:https://mathiasbynens.be/notes/shapes-ics

图片显示 TurboFan 根据执行中的分析数据优化字节码,之后的文本解释:

为了使其运行得更快,可以将字节码与分析数据一起发送到优化编译器。优化编译器根据其拥有的分析数据做出某些假设,然后生成高度优化的机器代码。

如果在某个时候其中一个假设被证明是不正确的,优化编译器将取消优化并返回到解释器。

重新分配函数指针时,会将冲突的分析数据从解释器发送到编译器。当您分配 undefined 时不会发生这种情况,因为在这种情况下不会执行该代码路径:if (p !== undefined) p();

即使我在 p 指向 func1 时调用它一次,然后将其分配给 func2 并在启动循环之前再次调用它,Chrome 上的性能损失仍然约为 2.5 毫秒。如果缓存已重置,我不明白损失。

你对V8的心理模型并不准确。 在某些情况下,它可以将JS进行JIT编译为本机机器代码,但是任何它无法处理的内容都会"取消优化"整个函数(或块或循环?)并使其解释。

我不是JS或V8的专家,但我已经掌握了一些细节。 谷歌发现了这一点: https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8

因此,这并不是说您一次使"缓存"无效,而是删除了它所基于的不变条件。

分支

预测作为分支目标的缓存仅在将 JS 运行时转换为本机机器代码而不是解释后才重要。 解释时,JS 中的控件依赖项只是在本机 CPU 上运行的解释器中的数据依赖项。

如果删除此不变性会取消优化函数或热循环,那么这就是您的问题,而不是分支预测。

最新更新