True, optimize code for JS engines is certainly an art form of its own. What usually works for me is to write boring imperative code when performance is a requirement. That seems to be easily optimizable by current engines.
Also, thanks for sharing my article here on HN. First time for me one of them is on HN.
It's not that hard to guess. Just look at what most NPM/Electron programmers and guides recommend you do, and then do the opposite. I.e., think about what your program is doing, and write the most boring, 90s-style code you can. Works really well internally for the Firefox codebase, and has been for 20+ years (for longer than it has even been called "Firefox").
Relevant talk: <https://www.youtube.com/watch?v=p-iiEDtpy6I>
Relevant paper: <https://users.dcc.uchile.cl/~rrobbes/p/EMSE-features.pdf>
Relevant thread: <https://news.ycombinator.com/item?id=33790568>
* Objects should have a fixed set of keys (no adding more and especially no `delete`) and the type of the value should never change (note: in JS, a type must NOT be a union of multiple types if you want it to optimize)
* Arrays must be of one type and a fixed length
* Functions must take a fixed number of arguments and the type of those arguments must never change. This is by far the most important if you want to get your function's code optimized.
And as a corollary from this article, it's not JS specific, but converting string -> float and float -> string isn't a cheap operation. I'd go so far as to say that it is the most expensive operation in a huge amount of the functions where it happens. Int to string is especially egregious because it requires a lot of division which is basically the slowest (common) thing you can do on a CPU.
If performance is critical then use a TypedArray where possible.
Your other points are spot on for high performance JS.
Declared in a fixed order if you want functions to remain monomorphic! One case where using classes is a bit safer than object literals.
In truth, the only likely situation from these rules is hand-copying configuration then manually transposing. This isn't likely to happen in really hot code paths.
This is the one place where JS helps us because lazy typers are encouraged to return object literals from factory functions and the return will almost always be one object returned at the bottom of the function.
It also (intentionaly) bypasses manually adding elements to an object directly after the object is instantiated. In truth, doing this consistently and directly after it is created can usually be optimized too, but not necessarily and describing this also complicates things (and varies from one JIT to the next).
Does that mean that tagged unions (functional programming-style data types) are inherently slow in JavaScript? Maybe there is a way for the TypeScript compiler to pass pragmas (via comments in generated code) to the interpreter.
The potential problem is that JS engines only see the ‘shape’ of an object. That is the members and member order. Which is slightly more strict that TS’s structural types where order doesn’t matter. So union variants and subtypes are each different shapes if they have different members or member order.
Shapes are important for function calls. Objects property access is optimised based on the number of ‘shapes’ that have been seen. Into the following:
Monomorphic - It’s seen only 1 shape and can access the properties by offset directly.
Polymorphic - A small inline cache of several shapes. Access is a lookup into this. Slightly slower.
Megamorphic - A lookup into a global table. This is a performance cliff.
So if you have lots of functions that take a tagged union or base class *and at runtime* they see lots of different types passed in *and this is in a hot path* they can be a problem.
Yeah. There's some great benchmarking and tips: https://www.zverovich.net/2013/09/07/integer-to-string-conve...
Godbolt example: https://godbolt.org/z/M4b353PKv of which the meat is
imul rcx, rcx, 1374389535
shr rcx, 37
imul edi, ecx, 100
mov r8d, edx
sub r8d, edi
movzx edi, WORD PTR .LC2[r8+r8]
mov WORD PTR [rsi], di
So if our example input is 12345, the first two instructions use the field-inverse property to compute "123" with multiply rather than divide (1 clock, latency 4), then multiply up again to get "12300", then subtract that to get "45". That can then be looked up at position 45+45 in the string "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899" to give you the two digits.I think this advice goes way back to before the 90s. I've also heard it called FIAL (Fortran In Any Language).
But, yeah. It's basically always the fastest thing possible in anything even approaching the mainstream.
But TS has a design constraint to be essentially a superset of JavaScript so it would be difficult to realize this goal.
This is polymorphic because union types are polymorphic.
let stringProcessor = (foo: string | number): string => {
//do stuff with foo
return "string" + foo
}
It's super easy to make non-optimized arrays and objects this way. type MyFoo = string | number //somewhere in the codebase
//you will get zero hints that this is a polymorphic function
let formatFoos = (foos: MyFoo[]) => foos.map((foo) => `${foo} deoptimized`))
Any use of optional object properties also immediately leads to polymorphism too. I doubt you can find ANY significant TS codebase where this isn't used pervasively. interface Foo {
foo: MyFoo //polymorphic
bar: string
baz?: number //this is polymorphic too
}
let doStuff = (obj: Foo): string {
//do stuff with obj
return "string"
}
doStuff({bar: "abc"})
doStuff({bar: "abc", baz: 123}) //we're now polymorphic
Even if we remove the `?`, it can still be polymorphic because the TS compiler doesn't reorder object properties when compiling. interface Foo {
bar: string //all basic types, so we're good?
baz: number //no more optional, so we're fine right?
}
let doStuff = (obj: Foo): string {
//do stuff with obj
return "string"
}
doStuff({baz: 123, bar: "abc"})
doStuff({bar: "abc", baz: 123}) //we changed order, so now we're now polymorphicDo you have a source for the above info? (both because I'm a little skeptical of some parts of it, but also because I'd love to learn more about this topic and I've had trouble finding concrete info in the past)
Viewed in those terms, it shouldn't be surprising that JS can be incredibly fast (faster than C in some cases).
Compared to what? Polar bears?
Here: is it faster than warm JVM?
You can't even store more than 14m or so keys in a map in Node w/o reaching for an off heap solution due to hard limits in the runtime...