我在 Chrome 的控制台中比较两个函数的性能时,偶然发现了一些我无法解释的东西。
const A = 3,
B = 2,
C = 4,
D = 2;
const mathCompare = (a1, a2, b1, b2) => {
return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};
const logicCompare = (a1, a2, b1, b2) => {
return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};
const runLooped = (j) => {
for(let i = 0; i < 4; i++) {
jsCompare(mathCompare, logicCompare, j);
}
}
const jsCompare = (f1, f2, iterations) => {
let a = jsPerf(f1, iterations);
let b = jsPerf(f2, iterations);
console.warn( iterations + " iterations:\n" + f1.name + ": " + a + " ms\n" + f2.name + ": " + b + " ms\n" + "delta: " + (a-b) + " ms");
}
const jsPerf = (f, iterations) => {
let start = performance.now();
for(let i = 0; i < iterations; i++) {
f(A, B, C, D);
}
return performance.now() - start;
}
runLooped(10000000);
运行 10M 迭代集 n 次:
由于性能仅在一组完整的迭代后才发生变化 - 一次调用 jsCompare() - 我决定再次尝试使用较少分解的结构:
const A = 3,
B = 2,
C = 4,
D = 2;
const mathCompare = (a1, a2, b1, b2) => {
return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};
const logicCompare = (a1, a2, b1, b2) => {
return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};
const compareRaw = (f1, f2, maxI, maxJ) => {
for(let i = 0; i < maxI; i++) {
let j,
a = performance.now();
for(j = 0; j < maxJ; j++) {
f1(A, B, C, D);
}
let b = performance.now();
for(j = 0; j < maxJ; j++) {
f2(A, B, C, D);
}
let c = performance.now();
console.warn( j + " iterations:\n" + f1.name + ": " + (b-a) + " ms\n" + f2.name + ": " + (c-b) + " ms\n" + "delta: " + ((b-a) - (c-b)) + " ms");
}
};
const runRaw = (i) => {
compareRaw(mathCompare, logicCompare, 4, i);
};
runRaw(10000000);
完全不同的结果。经过一些波动后,结果稳定在第 3 组附近。
我很好奇并再次尝试了一切,但这次是随机数。我正在测试的项目显然永远不会使用相同的参数调用这些函数 n * 10M 次。
const mathCompare = (a1, a2, b1, b2) => {
return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};
const logicCompare = (a1, a2, b1, b2) => {
return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};
const compareRawRandom = (f1, f2, maxI, maxJ) => {
let randoms = [...Array(maxJ + 3)].map(()=>Math.floor(Math.random()*10));
for(let i = 0; i < maxI; i++) {
let j,
a = performance.now();
for(j = 0; j < maxJ; j++) {
f1(randoms[j], randoms[j + 1], randoms[j + 2], randoms[j + 3]);
}
let b = performance.now();
for(j = 0; j < maxJ; j++) {
f2(randoms[j], randoms[j + 1], randoms[j + 2], randoms[j + 3]);
}
let c = performance.now();
console.warn( j + " iterations:\n" + f1.name + ": " + (b-a) + " ms\n" + f2.name + ": " + (c-b) + " ms\n" + "delta: " + ((b-a) - (c-b)) + " ms");
}
}
const runRawRandom = (i) => {
compareRawRandom(mathCompare, logicCompare, 4, i);
};
const jsCompareRandom = (f1, f2, iterations) => {
let randoms = [...Array(iterations + 3)].map(()=>Math.floor(Math.random()*10));
let a = jsPerfRandom(f1, iterations, randoms);
let b = jsPerfRandom(f2, iterations, randoms);
console.warn( iterations + " iterations:\n" + f1.name + ": " + a + " ms\n" + f2.name + ": " + b + " ms\n" + "delta: " + (a-b) + " ms");
}
const jsPerfRandom = (f, iterations, randoms) => {
let start = performance.now();
for(let i = 0; i < iterations; i++) {
f(randoms[i], randoms[i + 1], randoms[i + 2], randoms[i + 3]);
}
return performance.now() - start;
}
const runRandomLooped = (j) => {
for(let i = 0; i < 4; i++) {
jsCompareRandom(mathCompare, logicCompare, j);
}
}
runRandomLooped(10000000);
runRawRandom(10000000);
runRandomLooped() 为 mathCompare() 显示了同样奇怪的前 10M 设置。
runRawRandom() 分解较少的版本执行完全相同的计算,但是,在 2 组 10M 后再次稳定。但这一次,这两个函数在 ~23ms 的 10M 调用中表现出相同的性能。
这仅显示在 Chrome/Chromium 浏览器上。
测试:
我还在 Firefox 72.0.2 上进行了测试,它在集合和两种循环方式上都表现出稳定的性能。
我在当前的 Win10 上运行 AMD FX-8350。
我想这与 V8 在运行时的优化方式有关,但我不希望在这种情况下性能会下降。
最佳答案
V8 开发人员在这里。正如 wOxxOm 所指出的,这主要是对微基准测试陷阱的说明。
首先:
不,去优化不是这里的问题(这是一个非常具体的术语,具有非常具体的含义)。你的意思是“减速”。
不,高迭代次数也不是这里的问题。虽然您可以说测试中缺乏预热导致了您看到的结果,但您发现这种贡献并不那么令人惊讶。
需要注意的一种机制是“堆栈上替换”:具有(非常)长时间运行循环的函数将在循环执行时得到优化。在后台线程上执行此操作没有意义,因此在主线程上进行优化时会中断执行。如果循环之后的代码还没有执行,因此没有类型反馈,那么一旦执行到循环结束,优化的代码将被丢弃(“反优化”),以在执行未优化的同时收集类型反馈字节码。如果像这里的示例那样另一个长时间运行的循环,将重复相同的 OSR-then-deopt 舞蹈。这意味着您测量的一些重要部分是优化时间。这解释了您在时间稳定之前在 runRawRandom
中看到的大部分差异。
另一个需要注意的影响是内联。有问题的函数越小越快,调用开销就越大,在编写基准测试时可以避免这种情况,以便函数可以被内联。此外,内联通常会解锁额外的优化机会:在这种情况下,编译器可以在内联后看到从未使用过的比较结果,因此它会消除所有比较。这解释了为什么 runRandomLooped
比 runRawRandom
慢得多:后者对空循环进行基准测试。前者的第一次迭代是“快速”(=空),因为此时 V8 为 mathCompare
中的 f(...)
调用内联了 jsPerfRandom
(因为这是它在那里见过的唯一函数),但不久之后它意识到“哎呀,各种不同的函数是在此处被调用”,因此它会取消选择并且不会在后续优化尝试中再次尝试内联。
如果您关心细节,可以使用标志 --trace-opt --trace-deopt --trace-osr --trace-turbo-inlining --print-opt-code --code-comments
的某种组合来深入调查行为。请注意,虽然此练习可能会花费您大量时间,但您可以从微基准测试的行为中学到的东西很可能与实际用例无关。
为了显示:
mathCompare
比 logicCompare
但在实践中, 所有三个观察结果都是错误的 (鉴于其中两个相互直接矛盾,这并不过分令人惊讶):
关于javascript - Chrome 在高迭代次数下意外减速,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/60081883/