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

Why Does Reassigning Function Pointer Slow Down Function Call

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

下面是我用来测试这个场景的代码,这里是 fiddle。我递归循环 500 次并在每个循环中调用 p 1,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 引擎中的 Interpreter/compiler 管道”的部分:https://mathiasbynens.be/notes/shapes-ics

图片显示 TurboFan 根据执行分析数据优化字节码,其后的文字说明:

To make it run faster, the bytecode can be sent to the optimizing compiler along with profiling data. The optimizing compiler makes certain assumptions based on the profiling data it has, and then produces highly-optimized machine code.

If at some point one of the assumptions turns out to be incorrect, the optimizing compiler deoptimizes and goes back to the interpreter.

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

Even if I call p once while it points to func1 and then assign it to func2 and call it again before starting the loop, there is still a performance loss of about 2.5 ms on Chrome. If the cache has been reset, I don't understand the loss.

您对 V8 的心智模型不准确。它可以 JIT-compile 在某些情况下将 JS 转换为本机机器代码,但它无法处理的任何内容都会 "de-optimize" 整个函数(或块或循环?)并使其被解释。

我不是 JS 或 V8 方面的专家,但我了解了一些细节。 Google 发现了这个: https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8

所以这并不是说您要使 "the cache" 失效一次,而是要删除它优化所基于的不变条件。


分支预测作为分支目标的缓存仅在您将 JS 运行时 JIT 转换为本机代码而不是解释时才重要。解释时,JS 中的控制依赖只是原生 CPU.

解释器 运行 中的数据依赖

如果删除这个不变量 de-optimizes 函数或热循环,那是你的问题,而不是分支预测。