我在 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 次:
  • logicCompare() :所有集合都需要 ~170ms
  • mathCompare() :第一组需要 ~14ms ,接下来的组需要 ~600ms

  • 由于性能仅在一组完整的迭代后才发生变化 - 一次调用 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 组附近。
  • Chrome 79.0.3945.130:
  • logicCompare() :所有集合都需要 ~12ms
  • mathCompare() :所有集合都需要 ~10ms
  • Vivaldi 2.6.1566.49 (V8 7.5.288.30):
  • logicCompare() :所有集合都需要 ~60ms
  • mathCompare() :所有集合都需要 ~10ms

  • 我很好奇并再次尝试了一切,但这次是随机数。我正在测试的项目显然永远不会使用相同的参数调用这些函数 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 设置。
  • logicCompare() :所有集合都需要 ~280ms
  • mathCompare() :首先设置 ~27ms ,接下来设置 ~800ms

  • runRawRandom() 分解较少的版本执行完全相同的计算,但是,在 2 组 10M 后再次稳定。但这一次,这两个函数在 ~23ms 的 10M 调用中表现出相同的性能。

    这仅显示在 Chrome/Chromium 浏览器上。
    测试:
  • Chrome 79.0.3945.130
  • Vivaldi 2.6.1566.49(使用 V8 7.5.288.30)

  • 我还在 Firefox 72.0.2 上进行了测试,它在集合和两种循环方式上都表现出稳定的性能。
  • logicCompare() : ~110ms
  • mathCompare() : ~35ms

  • 我在当前的 Win10 上运行 AMD FX-8350。

    我想这与 V8 在运行时的优化方式有关,但我不希望在这种情况下性能会下降。

    最佳答案

    V8 开发人员在这里。正如 wOxxOm 所指出的,这主要是对微基准测试陷阱的说明。

    首先:



    不,去优化不是这里的问题(这是一个非常具体的术语,具有非常具体的含义)。你的意思是“减速”。



    不,高迭代次数也不是这里的问题。虽然您可以说测试中缺乏预热导致了您看到的结果,但您发现这种贡献并不那么令人惊讶。

    需要注意的一种机制是“堆栈上替换”:具有(非常)长时间运行循环的函数将在循环执行时得到优化。在后台线程上执行此操作没有意义,因此在主线程上进行优化时会中断执行。如果循环之后的代码还没有执行,因此没有类型反馈,那么一旦执行到循环结束,优化的代码将被丢弃(“反优化”),以在执行未优化的同时收集类型反馈字节码。如果像这里的示例那样另一个长时间运行的循环,将重复相同的 OSR-then-deopt 舞蹈。这意味着您测量的一些重要部分是优化时间。这解释了您在时间稳定之前在 runRawRandom 中看到的大部分差异。

    另一个需要注意的影响是内联。有问题的函数越小越快,调用开销就越大,在编写基准测试时可以避免这种情况,以便函数可以被内联。此外,内联通常会解锁额外的优化机会:在这种情况下,编译器可以在内联后看到从未使用过的比较结果,因此它会消除所有比较。这解释了为什么 runRandomLoopedrunRawRandom 慢得多:后者对空循环进行基准测试。前者的第一次迭代是“快速”(=空),因为此时 V8 为 mathCompare 中的 f(...) 调用内联了 jsPerfRandom(因为这是它在那里见过的唯一函数),但不久之后它意识到“哎呀,各种不同的函数是在此处被调用”,因此它会取消选择并且不会在后续优化尝试中再次尝试内联。

    如果您关心细节,可以使用标志 --trace-opt --trace-deopt --trace-osr --trace-turbo-inlining --print-opt-code --code-comments 的某种组合来深入调查行为。请注意,虽然此练习可能会花费您大量时间,但您可以从微基准测试的行为中学到的东西很可能与实际用例无关。

    为了显示:

  • 你在这里有一个微基准,毫无疑问地证明 mathComparelogicCompare
  • 慢得多
  • 你有另一个微基准,毫无疑问地证明两者具有相同的性能
  • 你的总体观察结果毫无疑问地证明,当 V8 决定优化事物时,性能会下降

  • 但在实践中, 所有三个观察结果都是错误的 (鉴于其中两个相互直接矛盾,这并不过分令人惊讶):
  • “快速结果”并不能反射(reflect)现实世界的行为,因为它们可以通过死代码消除您试图衡量的工作量
  • “缓慢的结果”并不能反射(reflect)现实世界的行为,因为基准测试的特定编写方式阻止了小函数的内联(实际上它总是会在实际代码中内联)
  • 所谓的“性能下降”只是一个微基准测试工件,根本不是由于失败/无用的优化,也不会在现实世界中发生。
  • 关于javascript - Chrome 在高迭代次数下意外减速,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/60081883/

    10-09 21:01