具有匿名函数性能的 JavaScript 循环

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)

执行速度要快得多。有人可以向我解释一下执行过程中发生了什么以及有什么区别吗?

Mor*_*ler 6

我认为这里的问题是JS引擎如何优化函数,但这与你直接传入箭头函数还是从指向函数的变量传入无关。

相反,在您的示例中,差异来自于调用函数的顺序filter()

首先声明三项免责声明:

  • 优化是由 JS 引擎完成的,所以你会在不同的引擎上看到不同的行为
  • 我绝不是该主题的专家,我想分享我的发现,以明确将箭头函数放入变量中不会对性能产生影响
  • 虽然有趣且具有教育意义,但除非您正在进行每一微秒都很重要的高性能计算,否则这对您编写代码的方式几乎没有影响。为其他程序员(包括未来的你)编写,而不是为编译器/解释器编写。

那么,为什么执行顺序对性能很重要呢?优化器使用的技巧之一是内联函数。我基本上意味着优化器可以通过函数的实现来替换对函数的调用。

所以这里的第二个函数:

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()函数并运行取消优化的代码。


很抱歉这篇文章很长,但我认为不要从像你这样的(非常好的)观察中吸取错误的教训,这一点非常重要。我希望观察结果表明,决定性能的并不是您使用箭头函数的方式。你自己尝试一下,如果你的发现与我的相符,请告诉我。