Watch the JIT

A cold loop on the JVM behind this site, compiled twice as it warms up, then thrown back to the interpreter when a new type reaches its call site.

Runs on
The JVM behind this site
Evidence for
JVM tuning, and mentoring mid-level engineers at Picsart
The code
4 Java files
Loading the experiment…This experiment runs in JavaScript, which is turned off. The notes below describe what it shows.

What you’re seeing

HotSpot runs a new method in its interpreter and compiles it once it turns out to be hot: first with C1, quickly and with profiling built in, then with C2, slowly and with everything the profile taught it. In the recorded run the first batch took about 355 µs; C2 had the loop by batch 9, and it settled at about 14.5 µs, some 24 times faster.

C2 bets on the profile. With one receiver type at the call site it inlines that type’s area() behind a type check. When a second type arrives the check fails, the compiled code is thrown away (a deoptimisation, reason class_check), the loop runs slowly for a few batches, and C2 compiles it again for two types. At four types it stops inlining and makes a virtual call, which settled about 2.6 times slower than the one-type loop.

How it works

Every run loads the workload through a class loader of its own, so its classes are new to the JVM: no profile and no compiled code, even on a server that has run them before. Flight Recorder’s jdk.Compilation and jdk.Deoptimization events are kept for that loader’s classes only and placed in the batch that was running. Batches run a millisecond apart, as requests would: back to back on one CPU, they would leave the compiler threads no time to work.

A JVM that has just started is most likely still compiling the server’s own start-up. In the recorded first run after a restart, C2 never reached the loop: after a few interpreted batches, every batch ran on C1’s code. The first requests after a deploy are slow for the same reason.

Where it meets my work

I mentor mid-level engineers at Picsart, and this is the picture I would draw for them: why a benchmark needs a warm-up, why the first requests after a deploy are slow, and how a hot call site gets slower when it meets more types, without a line of its code changing.

A shared vCPU makes the times noisy; the steps are the point. A batch is 20,000 calls of a small method and the loop around them, nothing else.

The code behind it

The files that do the work, by their place in this site’s repository.

  • api/src/main/java/com/aghazaryan/api/lab/jit/
    • JitRun.javaTimes a cold copy of the loop batch by batch, one, two, then four types
    • IsolatingClassLoader.javaDefines the workload anew for every run, so nothing about it is compiled yet
    • JitEvents.javaKeeps the run’s own compilations and deoptimisations from Flight Recorder
  • api/src/main/java/com/aghazaryan/api/lab/jit/workload/
    • Shapes.javaThe loop and the call site it watches