为什么 Array.prototype.fill() 与 `for` 循环相比有如此大的性能差异?

Why does Array.prototype.fill() have such a large performance difference compared to a `for` loop?

在对 Array.prototype.fill() 方法进行一些测试(Chrome 在 macOS 上)时,它显然比简单地创建自己的 for 循环慢将近两倍(如果不是更慢的话)并填充你的数组。

显然在做类似的事情:

for( var i = 0; i < Array.length; i++) {
   A[i] = 0;
}

Array.fill(0);

Array.fill() 方法需要约 210-250 毫秒来填充大小为 10000000 的数组,而 for 循环需要约 70-90 毫秒。似乎 Array.fill() 方法可以重写为简单地使用直接循环,因为您总是知道初始索引和目标索引。

let arrayTest = new Array(10000000),
    startTime,
    endTime;

startTime = performance.now();
arrayTest.fill(0);
endTime = performance.now();

console.log("%sms", endTime - startTime);
arrayTest = new Array(10000000);
startTime = performance.now();
for (let i = 0; i < arrayTest.length; i++){
  arrayTest[i] = 0;
}
endTime = performance.now();

console.log("%sms", endTime - startTime);

与我在本地测试时相比,以上实际上显示出更大的差异。

编辑:经过进一步测试后,我现在意识到,当切换到 Firefox 及其真正依赖于引擎时,差异会减少很多。我猜这主要是 JavaScript 引擎在优化循环与优化方法方面有何不同的结果。不过,似乎 Array.prototype.fill() 中的循环似乎仍可以优化以解决此差异。

结果与 Chrome 的部分内容在 JavaScript 中写入的报告一致,并依靠 运行 时间分析和优化来提高性能。

我将测试代码打包在一个函数中,以便从可以加载到不同浏览器中的测试页面重复调用(这不是 运行 可用的代码段):

<!DOCTYPE html>
<html><head><meta charset="utf-8">
<title>Array.prototype.fill</title>
<script>

Array.prototype.customFill = function( value, start = 0, end = this.length) {
    var count = end-start;
    if( count > 0 && count === Math.floor(count)){
        while( count--)
            this[start++]=value;
    }
    return this;
}

function test() {  
    let arrayTest,
        startTime,
        endTime,
        arraySize = 1000000;

    arrayTest = new Array(arraySize);
    startTime = performance.now();
    for (let i = 0; i < arrayTest.length; i++){
      arrayTest[i] = 0;
    }
    endTime = performance.now();
    console.log("%sms (loop)", endTime - startTime);

    arrayTest = new Array(arraySize);
    startTime = performance.now();
    arrayTest.fill(0);
    endTime = performance.now();
    console.log("%sms (fill)", endTime - startTime);

    arrayTest = new Array(arraySize);
    startTime = performance.now();
    arrayTest.customFill(0);
    endTime = performance.now();
    console.log("%sms (custom fill)", endTime - startTime);   
}
</script>
</head>
<body>
    open the console and click <button type="button" onclick="test()">test</button>
</body>
</html>

可以调整数组大小以适应所用设备的性能。

Windows 下 Chrome 的结果显示循环的性能大幅提升,前两次测试点击测试 。在第二次点击时,循环的时间似乎有所改善。在第三次单击时,循环和填充方法似乎都得到了优化,运行 的速度几乎相等,而且速度有所提高。重新加载页面后结果可重复。

我发现这与 Chrome 脚本优化策略一致,并且与 Chrome 的 Array.prototype.fill 是用 C++ 或类似语言编写的不一致。尽管 Array.prototype.fill.toString() 将函数体报告为 "native code" 但并未说明它是用什么语言编写的。


更新

为自定义填充方法添加计时,为提高速度而编写,并存储为 Array.prototype.customFill

Firefox 的时间与 Array.prototype.fill 以脚本编写的时间一致。本机实现优于循环,并且通常(但不总是)比自定义填充方法更快。

Chrome 节目的时间安排也与 Array.prototype.fill 以某种经过优化的脚本编写的时间一致。测试的所有三种填充方法都显示在一两次测试点击后速度有所提高。

但是,自定义填充方法的启动速度比 Chrome 的本机版本快十倍以上。您需要将无意义的代码放入自定义方法中,以使其足够慢以接近本机方法的初始速度。相反,经过优化后,本机方法的速度大约是原来的两倍 - JavaScript 中编写的自定义方法从未得到同样程度的优化。

虽然 Chromes Array.prototype.fill 方法可以写在 JavaScript 中,但似乎需要额外的解释来解释最初的缓慢和注意到的最终性能优化。

JSPerf 确认 fill 比 for 循环慢。

当数组大小为 1e5 时,fill 总是获胜。 当数组大小为1e7时,for