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.
Everyone who has ever used a web app already knows this unfortunately.
> 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).
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.
At any-rate, it’s not like copying will happen anyways during GC.
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’.
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].
What is mutable programming? Using mutable objects is something I do everyday. Am I doing things wrong?
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.
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-...
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.
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.
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.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
push() mutates an array in-place, and concat() completely duplicates the existing array and adds another array to it.
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