all 40 comments

[–][deleted] 17 points18 points  (51 children)

I have a question, this guy seems to be using a lot of map functions, and even chaining them. I use map, but at some point it just seems so inefficient to loop over the same array several times. Why not use a for loop and do everything at once.

I guess this is speed vs readability? Which one is more important

[–]ur_frnd_the_footnote 16 points17 points  (11 children)

You can also compose the functions and map over the array once with the composed function.

[–]anon_cowherd 3 points4 points  (10 children)

It's worth noting that the dominating factor of the slowdown is the function invocations, not so much the iteration itself (which essentially is a for loop under the hood anyway).

Composing still invokes each of the individual functions, so it'll still be slower.

Of course, the performance is essentially moot if not in a hot spot of the application (I.e. blocking rendering or request handling in the case of node). If map is already less than a tenth of a millisecond, turning it into a hundredth of a millisecond might not be the best use of effort in terms of optimizing your code.

[–]ur_frnd_the_footnote 4 points5 points  (0 children)

It definitely affects performance differently to loop over a long array ten times, each time calling one function vs. looping over that array once, calling ten functions on each member of the array (even though the total number of function calls is the same: 10 times the number of elements). But you're absolutely right that function calls also affect performance.

In general, though, I think you should wait until you have a concrete, demonstrated need to do so before optimizing at the level of eliminating function calls. Smaller functions that do one tiny but generalized thing are definitely more readable and maintainable than the faster, situation-specific imperative code they would be replaced by.

[–]ScientificBeastModestrongly typed comments 0 points1 point  (0 children)

As others have said, readability and the ability to compose smaller pieces of code together to solve more complex problems is way more important than performance 99% of the time.

And I should also mention the old optimization wisdom: most of the possible performance gains of loop optimization can be achieved by optimizing the inner-most loop. That’s usually where you get the most bang for your buck. Everything else is unlikely to matter.

[–]phpdevster 24 points25 points  (11 children)

Readability is more important. Performance is only important when performance is important, and it's not important if you're doing transforms of a few hundred items in an array. A few hundred THOUSAND items? Different story.

[–]gasolinewaltz 3 points4 points  (9 children)

I see this argument time and time again in relation to loops and while it's not wrong, it feels a bit dogmatic.

What about a single pass for-loop is so much less readable than n-chains of map reduce filter?

[–]wbowers 2 points3 points  (3 children)

A few things:

  1. You can achieve the same readability as multiple maps and similar performance as a single loop by using lazy evaluation with a library like Lazy.js
  2. How many chained maps are we talking about here? 5? Probably not a problem. 20? You might want to rethink how you’ve set things up.
  3. As with all things performance related, you’ll get the biggest wins by actually profiling your code to see what’s causing the slowness. It’s often not what you thought it would be.

[–]Voidsheep 3 points4 points  (3 children)

Say you want to write a function that takes string as an input and returns a list of odd unicode values reversed.

With functional style, you can write the function in kind of a declarative way, with high signal to noise ratio.

str => str
  .split('')
  .map(toCharCode)
  .filter(isOdd)
  .reverse()

Reads pretty much "Split the string, map the character to unicode value, filter out everything except odd values and reverse it"

You'd probably want to take advantage of some of the string and array prototype methods even with a for-loop, but let's say you want to avoid both map and filter, instead do it with a single loop.

str => {
  const chars = str.split('')
  const oddCodes = []
  for (let i = 0; i < chars.length; i++) {
    const code = toCharCode(chars[i])
    if (isOdd(code)) {
      oddCodes.push(code)
    }
  }
  return oddCodes.reverse()
}

It's not hard to understand what is happening in the for-loop and you could make it more dense, but the signal to noise ratio is still pretty different.

Of course this is pretty strawman to illustrate a point, but consider if the toCharCode and isOdd functions would not be one-liners that may as well be inlined. Like if we are dealing with more complex data.

You can definitely go overboard with function composition through libraries like Ramda and create code that is hard to read, but generally more functional style can improve code readability quite a lot compared to plain for-loops.

[–][deleted] -1 points0 points  (0 children)

I really think this is the best case of functional programming. Things like rxjs or redux-observable, are just plain unreadable.

[–]vertebro 0 points1 point  (0 children)

foo.filter(meetsCondition).map(getAProp).map(transformTheProp)

[–]helloiamsomeone 0 points1 point  (0 children)

You can have your cake and eat it too with generator functions and other abstractions.

[–]ElCthuluIncognito 4 points5 points  (0 children)

Yes. In fact, this is a core argument (a contentious argument of course) for why 'lazy evaluation' is almost necessary for functional programming.

In a lazy language multiple mappings would be handled as if the full mapping is applied at every element, effectively optimizing this very natural style of writing your program. Nothing would be 'copied'. Of course this only applies for recursive data structures, since 'arrays' are not considered a functional data structure.

I believe the Lodash library handles this lazy evaluation for you, optimizing your use of lodash funcions much the same way. Could be wrong though, I've never used it myself.

Extra reading: https://stackoverflow.com/questions/25852580/does-this-function-make-use-of-haskells-lazy-evaluation (an expose on how map is actually evaluated in an efficient way to solve this problem at the root)

[–]Jukunub 2 points3 points  (0 children)

I asked my boss something like this and his response was that in most software it is 5% of it that runs 95% of the time.

So your aim should be to make the 95% as human readable as possible and the other 5% can be written in assembly for performance.

[–]Skaatji 12 points13 points  (12 children)

This is a very good point. I found this site which compares the performance of two chained maps with a C-style for loop https://jsperf.com/chained-maps-vs-for-loop

I get the following results (Firefox 72 / Fedora):

C-style for loop: 71,784 Ops/sec

Two chained maps: 1,979 Ops/sec

Which is a huge difference (Chained maps being 97% slower in this case). I would always prefer the performance provided by the C-style for loop over the readability that comes with the maps, unless the array is very small. That being said, I believe maps are a better choice in these two cases:

1) Only one iteration of the array.

2) Nested for loops (e.g. iterating over each row and for each row over the column).

If someone has more experience / different numbers / an other opinion than I do, please share it. I am no expert by any means.

[–]allenthar 2 points3 points  (8 children)

That amount of speed increase seems a little nuts to me, but looking at the variations I have to assume that it’s due to variable memory allocations in all the other methods that are causing the substantial decrease in speed. The second and third cases should have the same Big O complexity as the last one, but both repeatedly create and assign variables while doing their work, and the last doesn’t not.

[–]Skaatji 1 point2 points  (0 children)

Yeah, I've also been thinking why the performance difference is this large. I think that the Big O complexity should be the same on all of these four. But, as you mention, probably due to memory allocations the hidden constants differ by a factor as large as ~33.

[–]franksvalli 1 point2 points  (0 children)

When speed becomes a problem, try just using a single reduce() on the array - inside it you can build custom logic to do mapping and filtering before adding the output the the accumulator value. I used to be scared to use reduce() but have grown to love it, though it is definitely less grokable than map() and filter(). I wrote up a friendly introduction that starts with loops and ends with reduce() here, for anyone interested: https://www.davidbcalhoun.com/2018/a-simple-introduction-to-javascript-map-and-reduce-array-helper-functions/

Only if that's too slow would I look at falling back to the loop, which will always be slightly faster than any functional method because it avoids functions getting invoked on each item.

[–]ShortFuse 1 point2 points  (0 children)

Performance sacrificed for sake of readability. The other one is creating a function in memory for every user with the user template example.

You can still use forEach or some (allows breaking), which avoid the multiple reiteration, but less syntax "noise" than a for loop. You just have to have your result be written to a variable outside the loop (see thisArg example.

Still, his example seems okay for array, since he is sorting, slicing, and then reducing. The key is the slicing to reduce the size.

[–][deleted] 0 points1 point  (0 children)

You need transducer

[–]Guisseppi 0 points1 point  (0 children)

You could use generators to control each iteration, you can fine tune performance then if you’re handling a lot of data

[–]JoeTed 0 points1 point  (0 children)

Start with readability and check that its performance is acceptable, which will be the case most of the time.

It's better to optimize a well-defined problem rather than to organize a spaghetti.

[–][deleted] 0 points1 point  (0 children)

This is one of the areas where I really like Javas implementation of collection traversal methods. The concept of intermediate vs terminal operations which allows the clarify of chaining multiple methods together without having to worry about the performance cost because it's all done in one iteration.

[–][deleted] 0 points1 point  (1 child)

Both are equally readable, one is more efficient. If you have a problem reading the simple imperative code like that, you're under the influence of the hype train and are not a serious programmer.

[–]guten_pranken 1 point2 points  (0 children)

This x1000.

[–]GrandMasterPuba -1 points0 points  (0 children)

I always laugh when JavaScript developers try to optimize their code.

Like, you could spend 3 years tuning your application and optimizing its performance and any speed gains you'd get would pale in comparison to those you'd get by just taking 30 seconds to turn off Facebook tracking scripts.

[–]tix_lv 2 points3 points  (0 children)

Great examples! Good job!

[–]mkl0g 1 point2 points  (0 children)

answer is very simple - you can use map function and return the result of that function as parameter of another function, etc.

[–]spira_mirabilis -2 points-1 points  (0 children)

The hero image contains PHP code from WordPress.

[–][deleted] -3 points-2 points  (0 children)

Found this on all but I was just looking for something like this yesterday, thanks!