Deep JavaScript: Theory and Techniques
exploringjs.com
exploringjs.com
(https://www.amazon.co.uk/JavaScript-Definitive-Guide-David-F...)
I also (briefly) interviewed him about it here: https://superhighway.dev/david-flanagan-interview
// Levels of protection: preventing extensions, sealing, freezing
Object.preventExtensions(obj)
Object.seal(obj)
Object.freeze(obj)JavaScript doesn't provide a nice way to extend the language syntax, unlike some other programming languages. So most DSLs end up looking like imperative code but do pure mutations underneath. I understand, for the reader it's confusing, because unless you know that it's a DSL you never know whether the mutations are pure or not.
Compare that with Haskell, with its support for custom operators, allows for some quite succinct code to express deep mutations (cf. lens library)
Some languages are typically taught in introductory programming courses - it's a very deliberate process aimed at explaining concepts from the ground up. JS is the opposite, people mostly learn it top-down, doing web development and being forced to use it.
And like eager college graduates need to learn it's sometimes a waste of time to optimize an algorithm, DIY JS devs can greatly benefit from a bit of theory and technical depth. Resources like this are perfect for that.
I can also wholeheartedly recommend Kyle Simpson's You Don't Know JS.
In fact, all the usual low-level optimization techniques like reducing branch mispredictions and expensive memory accesses apply, and make a huge difference, even though you're writing in a high-level language.
Still, it's interesting that there is something to be gained under these circumstances. I'm typically skeptical of this sort of thing because the standard library is written in C++. One time I thought I was very clever and hand-wrote a more appropriate sorting algorithm for a specific use-case only to discover that, no, Array.sort() was still faster by sheer brute force.
I remember the C# DirectX billboard sample, it was something like 10x slower than the same C++ sample in the same version of the SDK. Why? C# didn't have generics yet, and a value type was being stored in a non-generic list. Something like 4MB of memory was being copied around due to boxing and unboxing.
These things always matter.
However, JS doesn't primarily live in the server side world, it mostly lives in the browser world. And in the browsers, you rarely do heavy processing. If you are, you're doing it wrong - that logic needs to live on the server.
You're also doing it wrong in you are relying on server side NodeJS for tasks that involve heavy duty computations.
Still, interesting to know. I wonder if the author can make this repo a proposal to the TC39 committee.
A Node.js server's main thread is nickel and dimed by a thousand little cuts. A faster hash implementation can reduce loop delay by a nontrivial amount. This isn't heavy processing.
Same with the client. Any sort of game could have a good reason to be doing hashtable lookups in a hot loop on the client. This isn't heavy processing in some exotic use-case, it's rather elementary. And perf trade-offs especially help slow clients. And freeing up the main thread lets you fit in more nonreducible cycles.
Also, moving work to the server because your client implementation is too slow and then generalizing that to "always do work on the server", is tautological. Where you do work is fundamentally a business logic / product design concern that is only a performance concern in the suboptimal case where you can't fit the work on the server or client. So faster implementations move what are performance concerns back into the realm of higher level product design decisions.
These aren't symptoms of "doing it wrong". This is just plain jane software engineering.
Thanks, love this quote!
To use author's example, let's assume 2 scenarios.
Processing 4M records: 1) client side 2) server side via network API over the wire
One will be faster than the other. Not sure which one, but the slower one is what I would call "wrong."
This is very incorrect. At my last company we had a React interface (a specialized IDE, really) that needed to juggle (sort, filter, process) and work with sometimes hundreds of thousands of entities at once, all in-memory. JavaScript did this just fine, and the user experience (and the API design) really benefitted from not having a bunch of extra round-trips to the server.
Even in this extreme use-case, the bottleneck was always the few-hundred items that were actually rendered in the DOM at a time. We virtually never had performance problems from sheer volume of underlying data.
The author's example inserts 4M elements.
> and the user experience (and the API design) really benefitted from not having a bunch of extra round-trips to the server.
Possibly, but unless you benchmarked both scenarios, this might not be the case.
Performance over networked devices often has trade offs, sometimes those trade offs are directly contradictory, eg sorting/filtering/mapping/etc data on client side versus server side via api call - sometimes one is better than the other but you wont wont know unless you benchmark.
I get the impression that your company did not go through that exercise, considering you guys are building an IDE with scripting language.[1]
[1] https://nickjanetakis.com/blog/switching-to-vscode-from-subl....
The only reason the example inserts 4M elements was because Set and Object start to become prohibitively slow at some point and crash the process with too many allocations, not to mention the stress on the GC which now has to follow so many pointers.
HashTable performance is a fundamental component of any language.
Agreed, but why use Node if performance was so critical to your use case? Could this 400M insert logic exist in a separate service that lives outside your Node code base?
– The % operator calculates the remainder, not the modulus, which makes difference for negative numbers.
– Constructors can return a Promise.
– Function names are stored as the .name property.
`(~~4.2 === 4) && (~~-3.5 === -3)`
edit: It's like writing an intentional buffer overflow in C. You don't see it as often because it requires actually knowing how to program.
(4.2).toString(2);
> "100.00110011001100110011001100110011001100110011001101"
(~4.2).toString(2);
> "-101"
(~~4.2).toString(2);
> "100"
Perhaps the initial NOT is supposed to maintain the bits after the point? I have to go look at the spec.[Ok, looks like that not defined in the spec, and is a language-specific behavior: I have some homework now!]
It's just interesting that it works.
It appears my emotional response was not in line with everyone else's.