use*_*536 0 javascript performance
我有这个简单的代码(此代码的目的无关)
const filter = function (object, subject, match) {
const result = [];
let index = 0;
for (let i = 0; i < object.length; i++) {
let existingIndex = -1;
for (let j = index; j < subject.length; j++) {
if (match(object[i], subject[j])) {
existingIndex = j;
break;
}
}
if (existingIndex === -1) {
result.push(object[i]);
} else {
index = existingIndex;
}
}
return result;
};
const subjectArray = Array.from({ length: 199999 }).map((_, i) => i);
const objectArray1 = Array.from({ length: 99999 }).map((_, i) => i);
const objectArray2 = Array.from({ length: objectArray1.length }).map(() => -1);
const match = (item, entry) => item === entry;
function f1() {
console.time("f1");
filter(subjectArray, objectArray1, (item, entry) => item === entry);
console.timeEnd("f1");
}
function f2() {
console.time("f2");
filter(subjectArray, objectArray2, (item, entry) => item === entry);
console.timeEnd("f2");
}
f1();
f2();
Run Code Online (Sandbox Code Playgroud)
当我运行代码时,需要花费很多时间。但当我改变时
filter(subjectArray, objectArray2, (item, entry) => item === entry)
到
filter(subjectArray, objectArray2, match)
执行速度要快得多。有人可以向我解释一下执行过程中发生了什么以及有什么区别吗?
我认为这里的问题是JS引擎如何优化函数,但这与你直接传入箭头函数还是从指向函数的变量传入无关。
相反,在您的示例中,差异来自于调用函数的顺序filter()。
首先声明三项免责声明:
那么,为什么执行顺序对性能很重要呢?优化器使用的技巧之一是内联函数。我基本上意味着优化器可以通过函数的实现来替换对函数的调用。
所以这里的第二个函数:
const square = (x) => x*x
const doubleSquare = (x) => 2*square(x)
Run Code Online (Sandbox Code Playgroud)
可以内部优化为:
const doubleSquare = (x) => 2*x*x
Run Code Online (Sandbox Code Playgroud)
甚至在函数传入时也会发生这种情况。像这样的调用:
const doubleFun = (x, fun) => 2*fun(x)
doubleFun(3, square)
Run Code Online (Sandbox Code Playgroud)
可以触发优化,其中存储的版本doubleFun()与提供的函数内联,将其有效地变成与上面相同的形式:
const doubleFun = (x, fun) => 2*x*x*
Run Code Online (Sandbox Code Playgroud)
但是,如果您随后使用不同的回调调用该函数(例如doubleFun(3, Math.sign)),优化器必须返回,删除旧的内联函数并添加新的内联函数。这个过程称为去优化,需要一些时间。这就是您的代码中发生的情况,以及为什么第二次运行(巧合的是您的情况下带有内联箭头函数的版本)比第一次运行花费的时间更长。
你可以在node中实际观察这个过程,它使用Google的V8引擎。它分别使用--trace-opt和--trace-deopt标志输出优化和去优化通知。
我正在使用你的filter()函数并使用这些函数调用它们:
const match = (item, entry) => item === entry;
function withPointer() {
console.time("withPointer");
filter(subjectArray, objectArray, match);
console.timeEnd("withPointer");
}
function withInline() {
console.time("withInline");
filter(subjectArray, objectArray, (item, entry) => item === entry);
console.timeEnd("withInline");
}
console.log('GO pointer')
withPointer()
console.log('GO inline')
withInline()
Run Code Online (Sandbox Code Playgroud)
当我用 运行它时--trace-opt,我得到这个withInline():
GO inline
[found optimized code for 0x35b29fbc2ab1 <JSFunction filter (sfi = 0x38454569b381)> (target TURBOFAN) at OSR bytecode offset 102]
[compiling method 0x35b29fbc2ab1 <JSFunction filter (sfi = 0x38454569b381)> (target TURBOFAN) using Turbofan OSR]
[optimizing 0x35b29fbc2ab1 <JSFunction filter (sfi = 0x38454569b381)> (target TURBOFAN) - took 0.013, 2.027, 0.026 ms]
[marking 0x354e9b6c2051 <JSFunction (sfi = 0x3845456aaaf1)> for optimization to TURBOFAN, ConcurrencyMode::kConcurrent, reason: small function]
[compiling method 0x354e9b6c2051 <JSFunction (sfi = 0x3845456aaaf1)> (target TURBOFAN) using Turbofan]
[optimizing 0x354e9b6c2051 <JSFunction (sfi = 0x3845456aaaf1)> (target TURBOFAN) - took 0.006, 0.398, 0.011 ms]
[completed optimizing 0x354e9b6c2051 <JSFunction (sfi = 0x3845456aaaf1)> (target TURBOFAN)]
Run Code Online (Sandbox Code Playgroud)
忽略标记,您可以看到它首先优化filter()前三行中已知的函数,然后优化最后四行中的未命名函数。这是内联函数,它的优化与中的函数相同match(您可以以同样的方式观察到,为了简洁,我在这里省略了它)。
请允许我再次强调:匿名函数的优化与其他函数一样。
现在让我们用标志来运行整个过程--trace-deopt。输出是:
GO pointer
[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x28cb5c092131 <JSFunction filter (sfi = 0x3137cc314e49)>, opt id 2, bytecode offset 69, deopt exit 1, FP to SP delta 168, caller SP 0x7fffdc01a3a0, pc 0x000006bd6cd2]
withPointer: 213.071ms
GO inline
[bailout (kind: deopt-eager, reason: wrong call target): begin. deoptimizing 0x361b02a4ced1 <JSFunction filter (sfi = 0x3137cc314e49)>, opt id 3, bytecode offset 7, deopt exit 14, FP to SP delta 176, caller SP 0x7fffdc01a3a0, pc 0x000006bd7376]
withInline: 304.591ms
Run Code Online (Sandbox Code Playgroud)
我不知道第一条消息是什么意思,但是你可以看到,当第二次运行时,去优化正在运行。
让我们把它反过来并在指针版本之前运行内联版本:
GO inline
[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x1f1dddbd4561 <JSFunction filter (sfi = 0x30b415014f89)>, opt id 2, bytecode offset 69, deopt exit 1, FP to SP delta 168, caller SP 0x7ffe072c3630, pc 0x000006116cd2]
withInline: 209.497ms
GO pointer
[bailout (kind: deopt-eager, reason: wrong call target): begin. deoptimizing 0x20ca6714d511 <JSFunction filter (sfi = 0x30b415014f89)>, opt id 3, bytecode offset 7, deopt exit 14, FP to SP delta 176, caller SP 0x7ffe072c3630, pc 0x000006117376]
withPointer: 308.85ms
Run Code Online (Sandbox Code Playgroud)
与以前相同,第一次运行速度更快,第二次运行包括去优化步骤。
区别不在于内联与指针,而在于运行函数的顺序。
最后,我们看看是否可以通过filter(subjectArray, objectArray, (item, entry) => item === entry);在调用函数之前调用来预先创建一个优化版本:
filter(subjectArray, objectArray, (item, entry) => item === entry);
console.log('GO pointer')
withPointer()
console.log('GO inline')
withInline()
Run Code Online (Sandbox Code Playgroud)
事实上它给了我们:
[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x2b78d108bbc9 <JSFunction filter (sfi = 0x2826d0ed4f91)>, opt id 2, bytecode offset 69, deopt exit 1, FP to SP delta 168, caller SP 0x7ffc7a52db90, pc 0x000006216352]
GO pointer
[bailout (kind: deopt-eager, reason: wrong call target): begin. deoptimizing 0x11a825c8b6d1 <JSFunction filter (sfi = 0x2826d0ed4f91)>, opt id 3, bytecode offset 7, deopt exit 14, FP to SP delta 176, caller SP 0x7ffc7a52db30, pc 0x000006217136]
withPointer: 307.842ms
GO inline
withInline: 266.868ms
Run Code Online (Sandbox Code Playgroud)
因此,现在去优化发生在第一次调用时,而第二次调用则不间断地运行,并且比第一次调用更快。
通过在函数上方进行两次调用,函数期间不会发生去优化,并且我们得到几乎相同的运行时:
[bailout (kind: deopt-eager, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x0f7ddc811c99 <JSFunction filter (sfi = 0x104e5ad4f99)>, opt id 2, bytecode offset 69, deopt exit 1, FP to SP delta 168, caller SP 0x7ffc177e6eb0, pc 0x000006916352]
[bailout (kind: deopt-eager, reason: wrong call target): begin. deoptimizing 0x034c393079a9 <JSFunction filter (sfi = 0x104e5ad4f99)>, opt id 3, bytecode offset 7, deopt exit 14, FP to SP delta 176, caller SP 0x7ffc177e6eb0, pc 0x000006917136]
GO pointer
withPointer: 266.525ms
GO inline
withInline: 263.062ms
Run Code Online (Sandbox Code Playgroud)
这也让我们大致了解了优化代码、去优化代码和去优化本身的不同运行时:
| 跑步 | 持续时间[毫秒] |
|---|---|
优化filter() |
206 |
去优化filter() |
265 |
| 去优化 | 307 - 265 = 42 |
考虑到在您最初的示例中,如果您得到的数字与我相似,那么第二次调用的时间比第一次调用的时间要长 50% 左右,这样想是有道理的。第二次调用需要更多时间,因为它必须取消优化filter()函数并运行取消优化的代码。
很抱歉这篇文章很长,但我认为不要从像你这样的(非常好的)观察中吸取错误的教训,这一点非常重要。我希望观察结果表明,决定性能的并不是您使用箭头函数的方式。你自己尝试一下,如果你的发现与我的相符,请告诉我。
| 归档时间: |
|
| 查看次数: |
130 次 |
| 最近记录: |