node.js 和 chrome 中的循环优化问题?

Loop optimization issues in node.js and chrome?

偶然看到一句话,避免在每次迭代时读取数组的长度属性可以节省执行时间。
不过我觉得无所谓,做了个实验
然后遇到了问题
这是代码。

// Function to get the execution time.
function testFunction (func) {
    console.time(func.name)
    for (let i = 0; i < 100; i++) {
        func()
    }
    console.timeEnd(func.name)
}

// Init the array to iterate.
let arr = []
for (let i = 0; i < 10000000; i++) {
    arr.push(0)
}

function loopWithSavedLength () {
    let len = arr.length
    for (let i = 0; i < len; i++) {
        arr[i] = i
    }
}

function loopWithoutSavedLength() {
    for (let i = 0; i < arr.length; i++) {
        arr[i] = i
    }
}

testFunction(loopWithoutSavedLength)
testFunction(loopWithSavedLength)

而且输出很奇怪:

loopWithoutSavedLength: 889.633ms
loopWithSavedLength: 1023.269ms

我在Node.js 9.8.0(with v8 6.2.414.46-node.21)下试了很多次,loopWithoutSavedLength的执行时间总是比loopWithSavedLength的短。

我在 chrome 66.0.3359.181(使用 v8 6.6.346.32)控制台中执行相同的脚本,它们几乎相同。

loopWithoutSavedLength: 1475.060302734375ms
loopWithSavedLength: 1493.14892578125ms  

然后我认为可能是赋值和测试空循环有问题。
这是新代码。

function assignmentLoopWithSavedLength () {
    let len = arr.length
    for (let i = 0; i < len; i++) {
        arr[i] = i
    }
}

function assignmentLoopWithoutSavedLength () {
    for (let i = 0; i < arr.length; i++) {
        arr[i] = i
    }
}

function emptyLoopWithSavedLength () {
    let len = arr.length
    for (let i = 0; i < len; i++) {}
}

function emptyLoopWithoutSavedLength () {
    for (let i = 0; i < arr.length; i++) {}
}


testFunction(emptyLoopWithSavedLength)
testFunction(emptyLoopWithoutSavedLength)
testFunction(assignmentLoopWithSavedLength)
testFunction(assignmentLoopWithoutSavedLength)

node.js中的结果:

emptyLoopWithSavedLength: 580.978ms
emptyLoopWithoutSavedLength: 584.923ms
assignmentLoopWithSavedLength: 1046.899ms
assignmentLoopWithoutSavedLength: 901.542ms

chrome 控制台中的结果:

emptyLoopWithSavedLength: 584.126953125ms
emptyLoopWithoutSavedLength: 892.776123046875ms
assignmentLoopWithSavedLength: 1455.418212890625ms
assignmentLoopWithoutSavedLength: 1449.7529296875ms

后来我意识到存储在 arr 中的值可能会影响结果并且确实如此。

新代码在这里:

let arr = []
function initArr () {
    arr = []
    for (let i = 0; i < 10000000; i++) {
        arr.push(0)
    }
}

function testFunction (func) {
    initArr()
    console.time(func.name)
    for (let i = 0; i < 100; i++) {
        func()
    }
    console.timeEnd(func.name)
}

node.js中的结果:

emptyLoopWithSavedLength: 560.739ms
emptyLoopWithoutSavedLength: 1134.274ms
assignmentLoopWithSavedLength: 1841.544ms
assignmentLoopWithoutSavedLength: 1609.649ms

chrome 控制台中的结果:

emptyLoopWithSavedLength: 592.8720703125ms
emptyLoopWithoutSavedLength: 910.886962890625ms
assignmentLoopWithSavedLength: 1457.467041015625ms
assignmentLoopWithoutSavedLength: 1488.855224609375ms

现在有新问题出现。

结论:
1. 为什么在node.js中,无论数组元素是否初始化为0,loopWithoutSavedLength时间总是小于loopWithSavedLength?
2. 为什么创建新数组并将其元素初始化为 0 会产生不同的结果?

这里是 V8 开发人员。

问题1:loopWithoutSavedLength更快,因为访问元素需要执行边界检查,无论如何都需要加载长度。如果循环的条件已经包含相同的检查,则可以消除它。如果您保存长度,编译器将更难消除额外的检查。所以实际上,手动保存长度意味着重复工作。也就是说,差异通常太小而不重要(甚至可以衡量)。有关详细信息,请参阅 https://mrale.ph/blog/2014/12/24/array-length-caching.html 上的精彩文章。

问题2:我不确定。元素的值无关紧要;而在 Chrome 他们没有。 Node.js 中的 V8 与 Chrome 中的行为相同并且应该执行相同的操作;但也许您测试过的两个 V8 版本之间存在差异。我对你在 Node.js 中获得的 emptyLoopWithoutSavedLength 结果感到困惑,它显然毫无理由地从 584 变为 1134 -- 也许你系统中的其他原因导致了暂时的减速?你能重现这个结果吗?

说到重现:当我重复 运行 这些测试时,我看到重复 运行 相同测试的性能差异约为 10%(例如我得到的结果 assignmentLoopWithSavedLength 在 10 运行 的过程中介于 1075 和 1222 之间)。这并不罕见;现代计算机是复杂的机器,具有许多层和许多性能变化源。这只是意味着,当您执行两个测试中的单个 运行 时,您会看到 1400 和 1450 毫秒,这可能没有任何意义——下次它们可能会交换结果。如果你看到 1455 和 1449,差异几乎可以肯定是噪声。