Hacker News new | ask | show | jobs
by WorldMaker 1036 days ago
> If the engine doesn't do a full analysis up front but instead checks every time a type is used, you end up with, at best, roughly the same performance characteristics that current JIT engines already have.

There are definitely optimizations that JIT engines can do, pre-optimizing the "shapes" expected of objects, especially with respect to rare/optional parameters. In some of the current JITs an extra parameter that hasn't been used before/in-a-while drops code from a faster path to a slower one. Knowing ahead of time that parameter might show up eventually can lead to the faster path in more cases. You still get slow path penalties for type violations and other unexpected shapes, but that benefit of more "shapes" hitting the fast path sooner (less "learning time" for the expected shapes by the JIT, because it can assume the type is a correct description) may overall be a benefit over untyped performance.

Worst case, yeah, it is "roughly" the exact same performance characteristics, but best case it does buy some performance benefits.

That said, yeah the current proposal to TC-39 does not include that for a number of good reasons and it would need follow up proposals to provide type semantics that JITs could count on. Though if the first proposal succeeds, such follow up proposals become more likely. (We are seeing that a bit in Python as some follow up PEPs have converged some of the base type semantics, though still not yet with runtime performance in mind.)

1 comments

Does this mean the JIT engine will be compiling code that it would not have otherwise? That in itself might end up being a small penalty, given that so much code is never a hot path anyway.

If not, I'm not seeing the type annotation adding value that the engine doesn't already have from existing runs.

It should mean compiling (a lot) less code in the best cases. (JITs compile early and often.) Roughly, today:

1. JIT sees a function take a lot of objects {x: int, y: int}

2. JIT compiles a hot path of that function for {x: int, y: int}

3. JIT sees an object of {x: int, y: int, z: int}, goes to a slow uncompiled (deoptimized) path of that function

4. Over time JIT sees a bunch more of {x: int, y: int, z: int} and compiles a hot path for that function

5. Over time the JIT sees that the compiled hot paths of {x: int, y: int} and {x: int, y: int, z: int} share a bunch of code and get called roughly evenly and further compiles an even more optimized shared hot path of {x: int, y: int, z?: int}

Note that this isn't the case in every JIT, or every runtime and mileage always varies when talking about JIT optimizations, but that's a roughly common way to look at that.

In theory, knowing ahead of time that expected/preferred shape is {x: int, y: int, z?: int}, the JIT could skip steps 1-4, start from step 5, just one "perfect" hot path for the most common expected object shapes, and see fast code for every {x: int, y: int} and {x: int, y: int, z: int} object the function takes, right from "the beginning" of run time.

(It might still fall back to a deoptimized path for a strange, rare {x: string, y: int} or something like that, but it still has a better hot path for what should be the more common/likely arguments. Which is why worst case and possibly average case having type knowledge doesn't perform better than existing JITs. But it can still enhance the best case.)

(ETA: Of course, Step 0 is determining that function is on a hot path in the first place. That is assumed to be the same in both cases with/without type information.)