
JavaScript Array.push is 945x faster than Array.concat - turrini
https://dev.to/uilicious/javascript-array-push-is-945x-faster-than-array-concat-1oki
======
wutwut5521
These functions do different things though, right?

push() mutates an array in-place, and concat() completely duplicates the
existing array and adds another array to it.

    
    
        > x=[]; x.push(...[1,2]); console.log(x);
        [1, 2]
        > x=[]; x.concat([1]); console.log(x);
        []
    

So clearly push should be faster in general in usecases like these, since it
does not need to copy.

Edit: grammar and copy paste failures from my console

~~~
antihero
Yes, they are completely different functions with completely different
purposes. If you're still doing mutable programming for some god-forsaken
reason using concat to mutate an array is a misuse of the function.

~~~
PerfectElement
> If you're still doing mutable programming for some god-forsaken reason

What is mutable programming? Using mutable objects is something I do everyday.
Am I doing things wrong?

~~~
antihero
The idea of "editing" stuff instead of creating new versions of them.

Whilst mutable programming is faster on write, it is much more difficult to
figure out if something has changed, so any function that needs to only do
work when stuff has changed (e.g. a React component), it is much much better
to use immutable style programming because you only have to see if the memory
address has changed as opposed to deeply compare current and previous objects.

~~~
skohan
I disagree that mutable code is inherently faster to write. Like any paradigm
you can adopt, it probably feels that way igfyou start injecting immutability
into an existing project, but sooner or later you settle into different design
patterns which support it better, and it's not faster or slower to write.
Probably faster to debug though.

~~~
seanmcdirmid
Until hardware changes away from being an intrinsically imperative machines,
immutable approaches will be slower simply because the hardware doesn’t really
support that.

We saw FP hardware leading to performance boosts kind of happen with GPUs
(pixel and vertex shaders are just transformers), but then they got back to
imperative again with GPGPU.

------
imtringued
Am I seeing this right? Someone is creating 10000 arrays of increasing size
and then wonders why it's slow? Why does this even need benchmarks? It should
be obvious that copying 0.5 billion elements is far slower than only copying
10000.

~~~
barrkel
And some people object to testing knowledge of the basics of algorithm
complexity analysis with big-O in programming interviews because they "never
need that theory".

IMO complexity analysis is possibly the single most important bit of theory to
understand if you work on apps where there is any n which grows to a large
number. More important than being able to reverse a linked list or whatever.

------
_bxg1
Yeesh... this is why it's good to have some experience with a language where
you have to manage your own memory, even if you only write JavaScript at your
day job. To anyone who's written C++ before this difference is incredibly
obvious.

~~~
_bxg1
As a rule of thumb: anything done in a pure-functional/immutable style is
probably going to take a performance hit. This tradeoff is often worth it for
maintainability, especially if you're using true immutable data structures
like Immutable.js or Clojure's structures which reduce copying to the absolute
minimum. But always be aware of this tradeoff. If you're doing a simple
operation over 15,000 of anything, just use a for loop for goodness' sake.

~~~
barrkel
It really depends, I wouldn't be so forthright. In more complex apps, mutable
structures may be shared with other bits of code, and you may end up copying
for safety rather than because immutability forces you to. Non-local reasoning
is less reliable as complexity and team size (concurrent development!) scales
up, and the rules you adopt to ensure development can scale with larger teams
can hit you on the other side.

This is a distinct issue from the maintainability argument. I'm saying that if
you don't have functional code throughout, you might end up copying more often
than you think. Immutability means you can hang on to things and share them
with confidence.

For example, consider a method which returns an array in Java or C# or some
other language without type-based constness. It's not safe to cache that and
return the cached instance, because the array is mutable. So almost invariably
such arrays are constructed afresh every time - copying induced by lack of
immutable style.

~~~
_bxg1
> I'm saying that if you don't have functional code throughout, you might end
> up copying more often than you think. Immutability means you can hang on to
> things and share them with confidence.

But if you need to copy you're still following an "immutable" pattern: needing
to share data with the guarantee that it won't be changed. Copying is just a
very blunt way of accomplishing that. Immutable data structures will of course
be a big improvement over simple copying whenever you need to do this sort of
thing, but if you can avoid it altogether by modifying things in place instead
of relying on their lack of mutation, you'll have even less overhead.

Clojure's transients, for example, were created for exactly this purpose:
[https://clojure.org/reference/transients](https://clojure.org/reference/transients)

------
staeke
This is nonsense. You're micro-benchmarking some detail without reflecting on
how to use these methods. I take it you have this code (sort of)

    
    
      const all = [[1,2,3,4,5], [1,2,3,4,5], ...] //15000 items
      let ret = [];
      for (item of all) {
        ret = ret.concat(item);
        // Or ret.push(...item);
      }
    

The point of using concat is of course that you don't have to do the loop.
You're shuffling bytes for no use like crazy there. Not concat's fault. You
should be doing:

    
    
      [].concat(...all);
    

This runs in ~1ms on my machine (latest Chrome). Custom versions, and loop +
push both come it at around ~4ms.

------
janpot
Wondering how .flat() compares. [https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Refe...](https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat)

~~~
_bxg1
.flat() should be much better, because it "knows" it's flattening. In the
author's case, each individual step was treated as stateless, so a new array
had to be created each time. In a .flat() function, you could have a single
persistent, stateful destination array that everything gets copied into. It
would be equivalent to the performance of using push(). Actually, it would
probably be faster, because flat() is presumably implemented in native code
instead of JS.

------
lenticular
Check out Funkia List [0], an persistent O(log n) random access list
implementation. I use it pretty extensively when I'm writing typescript.

It actually can beat native arrays on concat and push, often by very wide
margins, while also being immutable. These kinds of operations are actually
much easier to optimize if you can assume immutability.

[0]
[https://funkia.github.io/list/benchmarks/](https://funkia.github.io/list/benchmarks/)

~~~
seanmcdirmid
It’s obvious how to make concat fast if one is willing to give up O(1) random
access operations. Push is already O(1).

~~~
lenticular
Modern persistent tree-based data structures are usually O(log32n) random
access, which is essentially O(1) for all practical purposes. With a mutable
array of references, random access follows one pointer, while these structures
follow usually less than 5. Sequential access is usually O(1).

These kind of persistent data structures are already the standard data
structures in Scala and Clojure, and they are fast enough for the vast
majority of non-numerical purposes. In typical access patterns, they are
faster than mutable arrays.

~~~
seanmcdirmid
As N approaches infinity O(log32 n) approaches O(log n). And really, we don’t
care much about N until it gets very large.

> In typical access patterns, they are faster than mutable arrays.

Only if typical is extremely random on large lists that consume many
pages/cache lines.

~~~
lenticular
Sequential access is O(1) just like mutable arrays. These structures really
are fast and practical. The benefits of immutability are enormous. That's why
they are so popular.

Edit: That's true about log32 and log2 become close in the limit, but that's
irrelevant for practical data sizes. For example,

log2(10^6) ~ 20,

log32(10^6) ~ 4

That's a 5x difference.

~~~
ericpauley
log32n/log2n is always 5, highlighting why this is such a meaningless
comparison. O(logn) (basically) refers to a constant multiple clogn, by which
logic O(log32n) = O(5log2n) = O(log2n) = O(logn). It's all the same.

Case in point, if your algorithm takes log32n operations but each OP takes 5x
longer its exactly the same as log2n. This is true for any value n, not just
large values.

~~~
seanmcdirmid
Ah! My explanation was wrong in that sense, thanks for the correction.

------
josephscott
[https://doesitmutate.xyz/](https://doesitmutate.xyz/)

------
KitDuncan
How about [...array1, ...array2] vs Array.concat. Or does 1 just use 2 under
the hood?

~~~
braythwayt
The issue here is that the author wants to merge one array into another, so
they wrote a = a.concat(b), but that is implemented as:

temp = a.concat(b) a = temp

In creating `temp`, JS copies all the elements of a and all the elements of b.

Whereas,

a.push(...b)

Only copies all the elements of b.

The code you propose copies all the elements of a and b, so it wouldn’t be
faster.

~~~
azhu
I believe you misunderstand. The question refers to whether or not using the
spread syntax only would result in similar performance gains as seen when
moving from .concat to .push with the spread.

const arraysToMerge = [ arr1, arr2, arr3, ... arrN ];

const spreadMergeFn = (reduced, arr) => [ ...reduced, ...arr ];

const spreadPushMergeFn = (reduced, arr) => reduced.push(...arr);

const merged = arraysToMerge.reduce(________, []);

Pure spreading will be slower. Using push lets you skip that first spread of
what you've accumulated so far in favor of mutating a reference. I suspect
that the above code using push will not run correctly though. Would need to
get under the hood of .reduce to see, but it should break.

My current personal opinion is to use flat() if possible.

// these produce identical data structures

[ arr1, arr2, arr3 ].flat()

[ ...arr1, ...arr2, ...arr3 ]

arr1.slice().push(...arr2.push(...arr3))

~~~
braythwayt
If you think a misunderstanding may be afoot, let me be as explicit as
possible. I do not believe that moving from:

a = a.concat(b)

To:

a = [...a, ...b]

Would give the same performance gains as moving to:

a.push(...b)

Because [...a, ...b] creates a new array, and copies the elements of both a
and b into it.

Old-timers will say that we had this exact same conversation about Java and
strings way back in the day. Using a StringBuffer was faster for this kind of
thing up and until Java started detecting when your use of string catenation
could be replaced by a StringBuffer.

------
im3w1l
Quadratic vs linear?

------
faragon
(push using geometric pre-reserve) vs ((exact-per-element growth + concat) x N
times) vs (exact reserve and then add the elements) . Being the third case
analogous to the "builder" concatenation technique (resize the element to
receive the concatenation for the final size, and copy the N strings into the
resized space).

The concat privitive not doing geometric pre-reserve is not a bad thing, in my
opinion, even if in this case is slower, because of memory saving for the most
frequent case. Of course, the concat operation should be always efficient when
destination container has enough space.

------
TehShrike
Watch out for this even more obscure gotcha – if you're dealing with large
enough arrays, V8 will throw an error:

    
    
       [].push(...new Array(120520))
    

results in `RangeError: Maximum call stack size exceeded` on node v10.15.3.

    
    
        [ ...new Array(120520)]
    

works fine though!

------
scj
Wouldn't a call to Array.prototype.splice act as a language defined (and
hopefully optimized) means of adding elements in-place?

Slightly ugly because you'd need to specify the start index, a command to
delete 0 elements, and a call/apply to inject the array, but it should do.

------
dickeytk
Look into Immutable.js if you want concatenation and the performance of
mutating

------
giacomorebonato
Now I have to change all my Redux reducers!

~~~
_bxg1
Redux is designed to be used with this kind of pattern. Immutable.js, which is
often paired with it, mostly solves this particular type of performance
problem. You'll still have a performance ceiling when you use Redux, but it's
very unlikely that you'll hit it.

Edit: I was using Immutable.js as a stand-in for immutable data structures in
general; apparently there are other JS libraries you can use

~~~
acemarke
Hi, I'm a Redux maintainer. I personally would recommend _against_ using
Immutable.js, for a number of reasons [0].

Instead, I highly recommend the Immer library [1], which lets you do immutable
updates with "mutating" logic in callbacks.

Even better, try out our new Redux Starter Kit package. It uses Immer
internally to let you simplify immutable updates in your reducers [2].

[0]
[https://www.reddit.com/r/javascript/comments/4rcqpx/dan_abra...](https://www.reddit.com/r/javascript/comments/4rcqpx/dan_abramov_redux_is_not_an_architecture_or/d51g4k4?context=3)

[1] [https://github.com/immerjs/immer](https://github.com/immerjs/immer)

[2] [https://redux-starter-kit.js.org/](https://redux-starter-kit.js.org/)

------
darepublic
I have often preferred concat over push for reasons of perceived elegance.

~~~
skohan
Performance optimization often comes at the cost of elegance. One is about
logical and conceptual purity, the other is about doing whatever is required
to achieve a result.

------
SoReadyToHelp
It's not 945x faster, it's O(N) faster.

~~~
StreamBright
945x is the actual measured difference, O(N) is the theoretical difference.
Many times quadratic time complexity is ok because you are working on very
small N.

~~~
olliej
The point is 945x is just the measurement for this particular test case. If
the author made the test larger it would appear even faster.

This is the underlying problem with people who don’t understand what big-O
complexity is saying.

------
boxingdog
well of course, one modifies the array and the other creates a new copy

------
bhalp1
Great research, great read.

~~~
netsharc
I couldn't stand the repetitive way she was presenting the information. Sure
it was like a monologue or conversation, but the info could've been given in a
paragraph or two.

------
revskill
OK, so i'll make a concat function which uses push under the hood. Best of
both worlds.

~~~
darepublic
They do different things so it's more like the worst of all worlds.

