我一直在使用函数指针p
来调用不同的函数。我的问题是,如果在为p
分配了一个函数之后将其指向另一个函数,则调用该函数的性能将大大降低。如果我一次又一次地在p
和同一个功能之间切换undefined
,则性能很好;当我仅将其指向一个功能,而在功能之间进行切换则会降低性能。
下面是我用来测试这种情况的代码,这里是fiddle。我递归循环500次,并在每个循环中调用p
1,000,000次。 p
可以是undefined
或指向func1
或func2
。
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
并返回原始功能却不会改变性能?
答案 0 :(得分:0)
您正在阻止引擎创建优化的热路径,因为它不能依赖函数指针的值。
请参见本文标题为“ JavaScript引擎中的解释器/编译器管道”的部分:https://mathiasbynens.be/notes/shapes-ics
图片显示了TurboFan基于执行中的数据分析来优化字节码,其后的文字说明:
为使其运行更快,可以将字节码与分析数据一起发送到优化编译器。优化编译器根据其具有的性能分析数据进行某些假设,然后生成高度优化的机器代码。
如果某个假设在某些时候被证明是不正确的,则优化的编译器会对图像进行反优化,然后返回解释器。
重新分配函数指针时,会将冲突的概要分析数据从解释器发送到编译器。当您分配undefined时,这不会发生,因为在这种情况下该代码路径不会执行:if (p !== undefined) p();
答案 1 :(得分:0)
即使我在指向func1的过程中调用一次p,然后将其分配给func2并在开始循环之前再次调用它,Chrome上仍然会损失约2.5 ms的性能。如果缓存已重置,我不知道丢失的原因。
您的V8心理模型不准确。在某些情况下,它可以将JS JIT编译为本地机器代码,但是它无法处理的任何事情都会“优化”整个函数(或者是块还是循环?)并进行解释。
我不是JS或V8的专家,但是我掌握了一些细节。 Google发现了这一点: https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8
因此,并不是要使“缓存”失效一次,而是要删除基于其优化的不变条件。
将分支运行作为分支目标的缓存的分支预测仅在将JS运行时从JIT转换为本机代码而不是解释时才有意义。解释时,JS中的控件依赖项仅是在本机CPU上运行的解释器中的数据依赖项。
如果删除此不变式会优化函数或热循环,那么这就是您的问题,而不是分支预测。