22

Most use cases of the reduce() method can be easily rewritten with a for loop. And testing on JSPerf shows that reduce() is usually 60%-75% slower, depending on the operations performed inside each iteration.

Is there any real reason to use reduce() then, other than being able to write code in a 'functional style'? If you can have a 60% performance gain by writing just a little bit more code, why would you ever use reduce()?

EDIT: In fact, other functional methods like forEach() and map() all show similar performance, being at least 60% slower than simple for loops.

Here's a link to the JSPerf test (with function calls): forloop vs forEach

Evan You
  • 2,701
  • 1
  • 24
  • 15

2 Answers2

7

The performance of the methods may vary depending on the size of the data. Speed ​​is also affected by compiler optimization and data warm-up. Therefore on small data for of wins, and on big reduce insignificantly wins.

You can see for yourself by running the test:

const LOOP = 3

test(dataGenerator(5))
test(dataGenerator(500))
test(dataGenerator(50000))
test(dataGenerator(500000))
test(dataGenerator(5000000))

function test(dataSet) {
    let sum

    console.log('Data length:', dataSet.length)

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} reduce`)
        sum = dataSet.reduce((s, d) => s += d.data, 0)
        console.timeEnd(`${x} reduce`)
    }

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} map`)
        dataSet.map((i) => sum += i.data)
        console.timeEnd(`${x} map`)
    }

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} for loop`)
        for (let i = 0; i < dataSet.length; i++) {
            sum += dataSet[i].data
        }
        console.timeEnd(`${x} for loop`)
    }

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} for reverse`)
        for (let i = dataSet.length; i--;) {
            sum += dataSet[i].data
        }
        console.timeEnd(`${x} for reverse`)
    }

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} for of`)
        for (const item of dataSet) {
            sum += item.data
        }
        console.timeEnd(`${x} for of`)
    }

    for (let x = 0; x < LOOP; x++) {
        sum = 0
        console.time(`${x} for each`)
        dataSet.forEach(element => {
            sum += element.data
        })
        console.timeEnd(`${x} for each`)
    }

    console.log()
}

function dataGenerator(rows) {
    const dataSet = []
    for (let i = 0; i < rows; i++) {
        dataSet.push({id: i, data: Math.floor(100 * Math.random())})
    }
    return dataSet
}

These are the results of a performance test on my laptop. for loop does not work stably unlike for reverse and for of.

➜  node reduce_vs_for.js 
Data length: 5
0 reduce: 0.127ms
1 reduce: 0.008ms
2 reduce: 0.006ms
0 map: 0.036ms
1 map: 0.007ms
2 map: 0.018ms
0 for loop: 0.005ms
1 for loop: 0.014ms
2 for loop: 0.004ms
0 for reverse: 0.009ms
1 for reverse: 0.005ms
2 for reverse: 0.004ms
0 for of: 0.008ms
1 for of: 0.004ms
2 for of: 0.004ms
0 for each: 0.046ms
1 for each: 0.003ms
2 for each: 0.003ms

Data length: 500
0 reduce: 0.031ms
1 reduce: 0.027ms
2 reduce: 0.026ms
0 map: 0.039ms
1 map: 0.036ms
2 map: 0.033ms
0 for loop: 0.029ms
1 for loop: 0.028ms
2 for loop: 0.028ms
0 for reverse: 0.027ms
1 for reverse: 0.026ms
2 for reverse: 0.026ms
0 for of: 0.051ms
1 for of: 0.063ms
2 for of: 0.051ms
0 for each: 0.030ms
1 for each: 0.030ms
2 for each: 0.027ms

Data length: 50000
0 reduce: 1.986ms
1 reduce: 1.017ms
2 reduce: 1.017ms
0 map: 2.142ms
1 map: 1.352ms
2 map: 1.310ms
0 for loop: 2.407ms
1 for loop: 12.170ms
2 for loop: 0.246ms
0 for reverse: 0.226ms
1 for reverse: 0.225ms
2 for reverse: 0.223ms
0 for of: 0.217ms
1 for of: 0.213ms
2 for of: 0.215ms
0 for each: 0.391ms
1 for each: 0.409ms
2 for each: 1.020ms

Data length: 500000
0 reduce: 1.920ms
1 reduce: 1.837ms
2 reduce: 1.860ms
0 map: 13.140ms
1 map: 12.762ms
2 map: 14.584ms
0 for loop: 15.325ms
1 for loop: 2.295ms
2 for loop: 2.014ms
0 for reverse: 2.163ms
1 for reverse: 2.138ms
2 for reverse: 2.182ms
0 for of: 1.990ms
1 for of: 2.009ms
2 for of: 2.108ms
0 for each: 2.226ms
1 for each: 2.583ms
2 for each: 2.238ms

Data length: 5000000
0 reduce: 18.763ms
1 reduce: 17.155ms
2 reduce: 26.592ms
0 map: 145.415ms
1 map: 135.946ms
2 map: 144.325ms
0 for loop: 29.273ms
1 for loop: 28.365ms
2 for loop: 21.131ms
0 for reverse: 21.301ms
1 for reverse: 27.779ms
2 for reverse: 29.077ms
0 for of: 19.094ms
1 for of: 19.338ms
2 for of: 26.567ms
0 for each: 22.456ms
1 for each: 26.224ms
2 for each: 20.769ms
5
  • You might want scoping. For example you might want to make callback functions or have references to javascript objects. For more information, see why javascript is not blocked scoped. [edit: modern javascript now supports let variables. Back before ESv6, when you declared a var variable, it got hoisted as if it was written at the top of the function codeblock, so often you had to write bodies of for-loops as functions. This following still applies though:] If you had a function written, might as well use functional style unless it's a significant bottleneck.
  • Your code does not always need to run at full machine speed. You may not even be optimizing code in the bottleneck.
  • Additionally you do not provide your "testing on JSPerf" so we can critique it. For example if you already have a reduction function (or map or forEach function), then I bet the performance would be on-par. Even if not, the testing methodology may be flawed, especially given that many browsers may optimize differently or have different function-call overhead.

sidenote: this is a valid performance comparison between syntax, but an invalid performance comparison in when syntax is not the question at hand:

myArray.map(function(x){return x+1})

// ...versus...

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

This would be a valid performance comparison:

myArray.forEach(function(x){return x+1})

// ...versus...

var plusOne = function(x){return x+1};
for(var i=0; i<myArray.length; i++) {
    plusOne(myArray[i]);
}

// (may need a side-effect if the compiler is smart enough to optimize this)

(Also in reply to your edit: .forEach() and .map() provide much more clarity, and avoid the need for explicit loop int i=0; i<array.length; i++ arguments.)

ninjagecko
  • 88,546
  • 24
  • 137
  • 145
  • 2
    I disagree with you about what constitutes a valid performance comparison. The _first_ is the valid comparison, because surely (for your example) the question is "Which method is fastest at adding one to every element of the array?", not "Which method is fastest but any method not using a function call is disqualified from entry". I would agree that if you assume you want a function anyway for scoping/closure purposes then you might as well use `.reduce()` or `.forEach()` or whatever. – nnnnnn Mar 09 '12 at 06:57
  • The question is exactly about the syntax, so it is a valid performance comparison. Perhaps I should rephrase the question in a scenario where you want to squeeze every possible bit of performance. In that case I can't find a good reason to not use a plain for loop. Can you provide a concrete example in which scoping is absolutely needed? – Evan You Mar 14 '12 at 01:17
  • @EvanYou: `[1,2,3].forEach(function(x){setTimeout(function(){alert(x)},1000)})` (alerts 1,2,3) versus `for(var i=0;i<4;i++){setTimeout(function(){alert(i)},1000)}` (alerts 4,4,4) – ninjagecko Mar 14 '12 at 02:13
  • @ninjagecko Thanks, that makes sense. However we can also provide scope by creating a function to be called within each for loop, like the second test case in your original answer. I did a similar one on JSPerf and it turns out that forEach is still 60% slower: [test here](http://jsperf.com/forloop-vs-foreach12) – Evan You Mar 14 '12 at 04:59
  • Actually the performance difference is smaller when the number of elements in the array increases (at least in Chrome), however it is still significant. – Evan You Mar 14 '12 at 05:07
  • 2
    Using `let` instead of `var` in a for loop makes your first bullet invalid in modern implementations of JS (just leaving this here for people reading this question in 2017). – Timothy Zorn Aug 23 '17 at 12:54
  • The last examples `forEach` and the `for` loop aren't valid performance wise, as the VM would simply skip them, given that `forEach` cannot return values and the `plusOne` return is not being assigned to anything – Drenai May 26 '19 at 20:14
  • That was exactly what the following `// ...` said in the answer; the VM may or may not or may not even be authorized to optimize it. You can tell if it's valid with your particular VM by seeing if it uses your CPU for a bit or merely returns instantly without doing and work. – ninjagecko May 27 '19 at 16:35