JavaScript Array.push is 945x faster than Array.concat
dev.to
dev.to
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
What is mutable programming? Using mutable objects is something I do everyday. Am I doing things wrong?
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.
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.
IMHO, if you're doing deep compare on anything for any reason, it's usually a sign that the data model is on a shaky ground.
Here's some good overviews of why and how to do immutable updates in JS:
https://redux.js.org/faq/immutable-data
https://redux.js.org/recipes/structuring-reducers/immutable-...
No. Mutating state that is shared between different parts of a program is often a bad idea. The functional programming community learned the first half of that, and now goes round preaching the mistaken idea that all mutation is bad.
For example, if I pass an argument into a function, it may be 'unexpected' that the argument is mutated - I can not reason about that mutation locally (unless it's very explicit or a known idiom such as push).
However, within a function, avoiding mutation seems pointless as you should have no trouble reasoning about it. At some point you really are just throwing away performance with significantly diminishing benefits.
Shared mutability across threads is definitely a huge pain in the ass though.
In the end I think we're all just trying to reduce the state space we have to manage in our heads when we read and write code, and removing mutability reduces that space.
Ok, but here you are doing all the manual work of creating a copy as to avoid mutating the arg/returning a new one and, it may be less peformant because of whole copy, knowing your programming language automtically defaults and does this for you in a performant way is a big win for reducing cognitive overhead in large programs.
I do agree that copying stuff around is generally expensive. I am just unsure how expensive it is in smaller functions that aren't called 10_000 times a second.
I believe one such "god-forsaken reason" was given to us by the title of the link...
> JavaScript Array.push is 945x faster than Array.concat
Another big problem is that the article's benchmark is busted [1]. The author thought they were just concatenating two arrays of a fixed length a bunch of times. But what's actually happening is that arr1 is being built up because it is reused for each test case. That means that the concat version is doing A LOT of copying of the data. If you fix the test so that each run concatenates only two 50k arrays, concat is faster [2].
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Here's a stackoverflow answer which talks a bit more about this: https://cs.stackexchange.com/a/9382
The constant bound on the number of copies depends on the growth factor. With a growth factor of 2, the constant is 2. As long as the growth factor is greater than 1, the number of copies is linear.
You can verify this yourself with some trivial code:
struct CountsCopies {
CountsCopies() {}
CountsCopies(const CountsCopies& other) { ++CopyCount(); }
CountsCopies& operator=(const CountsCopies& other) { ++CopyCount(); return *this; }
static std::size_t& CopyCount() {
static std::size_t count = 0;
return count;
}
};
std::size_t last_copies = 0;
std::vector<CountsCopies> v;
for (int i = 0; i < 1000; ++i) {
const std::size_t copies = CountsCopies::CopyCount();
if (copies != last_copies) {
std::cout << "size=" << i << " copies=" << copies
<< " ratio=" << copies / static_cast<double>(i) << '\n';
last_copies = copies;
}
v.emplace_back();
}
Output: size=2 copies=1 ratio=0.5
size=3 copies=3 ratio=1
size=5 copies=7 ratio=1.4
size=9 copies=15 ratio=1.66667
size=17 copies=31 ratio=1.82353
size=33 copies=63 ratio=1.90909
size=65 copies=127 ratio=1.95385
size=129 copies=255 ratio=1.97674
size=257 copies=511 ratio=1.98833
size=513 copies=1023 ratio=1.99415It's O(N) for any geometric growth rate, but using 2x makes it easy to see, because every copy is one larger than all previous copies combined. Consider a concrete example for N=9: 1 + 2 + 4 + 8 = 7 + 8.
I don’t know whether it is done in practice, but you can postpone doing that until the array gets ‘big’, for some definition of ‘big’.
At any-rate, it’s not like copying will happen anyways during GC.
No, because performance is generally not a huge concern on the front-end. I'm not applying ML strategies to hundreds of thousands of data points, I'm trying to render 10 elements instead of 9. Performance is so rarely a concern that I'd always err on the side cleaner code than hyper-performant code. This stuff isn't even worth thinking about.
> I'm not applying ML strategies to hundreds of thousands of data points
Maybe you aren't, but the folks over at tensorflow.js [1] certainly are, as well as Andrej Karpathy's ConvnetJS [2].
[1] https://www.tensorflow.org/js [2] https://cs.stanford.edu/people/karpathy/convnetjs/demo/class...
If more front-end developers have this mindset, I'm beginning to understand how the web (and the desktop, via electron and such) has become the embarrassingly slow and unusable mess that it is nowadays.
I am a fan of expressive and readable code as well and have fully migrated to functional languages in the last 2-3 years. But I don't think in JS we have the luxury to ignore a 945x performance improvement on a very low-level building-block function. It's used in thousands of places.
So you know, I agree with your premise. As a compromise I'd make an utility that reads much better than `Array.push` but still uses it internally (if such a tool does not already exist).
Everyone who has ever used a web app already knows this unfortunately.
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.
Unless you're on Python... /s
When it happens that parts of this work later turn out not to be needed, it's easy to discard an enumerable, and the iteration is not actually performed. It can sometimes lead to really ellegant code that also performs well.
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.
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
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.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.
More sophisticated persistent structures like Funkia List have O(log32n) access, which is basically constant time. This makes them better general-purpose data structures than mutable arrays.
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.
> 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.
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.
There is a good reason why the subscript of log is often not even mentioned. Logarithmic is logarithmic, no longer what subscript you bother in investing extra space for.
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.
In the major JS engines string concat is essentially O(1) because strings are immutable (and they flatten them out as appropriate according to whatever heuristics make sense).
But for array concatenation in js i can do
a.concat(b)
Followed by
a[0]=something
Or
Delete a[1]
Or
A.push(something)
Or a.length++
Etc
To make this particular array concat fast would require significant perf impact to all other arrays, even those that aren’t involved in concat
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.
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))
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.
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.
[].push(...new Array(120520))
results in `RangeError: Maximum call stack size exceeded` on node v10.15.3. [ ...new Array(120520)]
works fine though!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.
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
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...
This is the underlying problem with people who don’t understand what big-O complexity is saying.