\

JIT: So you want to be faster than an interpreter on modern CPUs

129 points - last Sunday at 7:08 PM

Source
  • klipklop

    yesterday at 10:21 PM

    A shame operating systems like iOS/iPadOS do not allow JIT. iPad Pro's have such fast CPU's that you cant even use fully because of decisions like this.

      • ivankra

        today at 9:59 AM

        They allow, but Apple's policy is to lock down that ability pretty much just to Safari/WKWebView. If you could compile your program to JS or WASM and run it through one of these blessed options, it should get JIT'ted.

        • Pulcinella

          yesterday at 11:41 PM

          Those operating systems allow it, but Apple does not. Agree that it is a total waste.

          • pjscott

            today at 3:56 AM

            They do, technically, allow JIT. You need a very hard-to-obtain entitlement that lets you turn writable pages into executable read-only pages, and good luck getting that entitlement if (for some reason) your name isn’t “mobilesafari”, but the capability exists.

            • duped

              today at 12:40 AM

              What advantage does JIT compilation have over Swift or Obj-C?

                • bencyoung

                  today at 8:26 AM

                  JIT compilation can be faster for compiled languages too, as it allows data driven inlining and devirtualization, as well as "effective constant" propogation and runtime architecture feature detection

                  • saagarjha

                    today at 12:54 AM

                    It speeds up interpreted languages.

                      • Pulcinella

                        today at 12:58 AM

                        And emulation.

                          • saagarjha

                            today at 1:01 AM

                            What is an architecture but a scripting language to interpret? ;)

                • almostgotcaught

                  today at 3:11 AM

                  > that you cant even use fully because of decisions like this.

                  Have no clue what this means - you can pre-compile for target platforms and therefore "fully" use whichever Apple device CPU.

                    • pjc50

                      today at 8:54 AM

                      You can't precompile your web page's Javascript for iOS, even if you're willing to have it signed and notarized and submitted for policy review.

                      • today at 8:53 AM

                • stmw

                  yesterday at 10:15 PM

                  Good read. But a word of caution - the "JIT vs interpreter" comparisons often favor the interpreter when the JIT is inplemented as more-or-less simple inlining of the interpreter code. (Here called "copy-and-patch" but a decades-only approach). I've had fairly senior engineers try to convince me that this is true even for Java VMs. It's not in general, at least not with the right kind of JIT compiler design.

                    • hoten

                      yesterday at 10:58 PM

                      I just recently upgraded[1] a JIT that essentially compiled each bytecode separately to one that shares registers within the same basic block. Easy 40 percent improvement to runtime, as expected.

                      But something I hadn't expected was it also improved compilation time by 40 percent too (fewer virtual registers made for much faster register allocation).

                      [1] https://github.com/ZQuestClassic/ZQuestClassic/commit/68087d...

                        • chromatic

                          today at 2:17 AM

                          This is an embarrassing context to admit, but here goes.

                          Back when Parrot was a thing and the Perl 6 people were targeting it, I profiled the prelude of Perl 6 to optimize startup time and discovered two things:

                          - the first basic block of the prelude was thousands of instructions long (not surprising) - the compiler had to allocate thousands of registers because the prelude instructions used virtual registers

                          The prelude emitted two instructions, one right after another: load a named symbol from a library, then make it available. I forget all of the details, but each of those instructions either one string register and one PMC register. Because register allocation used the dominance frontier method, the size of the basic block and total number of all symbolic registers dominated the algorithm.

                          I suggested a change to the prelude emitter to reuse actual registers and avoid virtual registers and compilation sped up quite a bit.

                      • _cogg

                        yesterday at 11:00 PM

                        Yeah, I expect the real advantage of a JIT is that you can perform proper register allocation and avoid a lot of stack and/or virtual register manipulation.

                        I wrote a toy copy-patch JIT before and I don't remember being impressed with the performance, even compared to a naive dispatch loop, even on my ~11 year old processor.

                          • ack_complete

                            today at 3:25 AM

                            The difference between interpreters and simple JITs has narrowed partly due to two factors: better indirect branch predictors with global history, and wider execution bandwidth to absorb the additional dispatch instructions. Intel CPUs starting with Haswell, for instance, show less branch misprediction impact due to better ability to predict jump path patterns through the interpreter. A basic jump table no longer suffers as much compared to tail-calling/dispatch or a simple splicing JIT.

                            • stmw

                              today at 2:44 AM

                              Exactly, and it's not just register allocatio: but for many languages also addign proper typing, some basic data flow optimization, some constant folding, and a few other things that can be done fairly quickly, without the full set of trees and progressive lowering of the operators down to instruactions.

                              What's odd about the "JIT vs interpreter" debate is that it keeps coming up, given that it is fairly easy to see even in toy examples.

                      • gr4vityWall

                        yesterday at 8:50 PM

                        That was a pretty interesting read.

                        My take is that you can get pretty far these days with a simple bytecode interpreter. Food for thought if your side project could benefit from a DSL!

                        • neerajsi

                          today at 1:21 AM

                          From the previous article in the series, it looks like the biggest impediment to just using full llvm to compile the query is that they didn't find a good way to cache the results across invocations.

                          Sql server hekaton punted this problem in a seemingly effective way by requiring the client to use stored procedures to get full native compilation. Not sure though if they recompile if the table statistics indicate a different query plan is needed.

                          • gary_0

                            today at 12:12 AM

                            > This is called branch prediction, it has been the source of many fun security issues...

                            No, that's speculative execution you just described. Branch prediction was implemented long before out-of-order CPUs were a thing, as you need branch prediction to make the most of pipelining (eg. fetching and decoding a new instruction while you're still executing the previous one--if you predict branches, you're more likely to keep the pipeline full).

                              • Arnavion

                                today at 12:53 AM

                                Speculative execution does not require out-of-order execution. When you predict a branch, you're speculatively executing the predicted branch. Whether you're doing it in the same order as instruction order or out of order is independent of that.

                                • monocasa

                                  today at 1:59 AM

                                  Essentially all microarchtectural state is fodder for side channel exploits.

                                  Static branch prediction like "predict taken if negative branch offset" doesn't leak anything, but just about any dynamically updated tables will (almost tautologically) contain statistical information about what was executed recently.

                              • scrash

                                today at 4:14 AM

                                The issues with branch prediction aren't really as much of a thing in modern interpreters, I can really recommend reading https://inria.hal.science/hal-01100647/document

                                  • titzer

                                    today at 5:57 AM

                                    The paper is 10 years old. While the gap between a threaded an interpreter (a dispatch at the end of every handler) versus non-threaded (loop over switch) isn't as big as it used to be, it's still 15-30% on modern very fast interpreters. For example, I measured between 14 and 29% performance improvement for threading Wizard's interpreter[1].

                                    [1] https://dl.acm.org/doi/10.1145/3563311

                                • imtringued

                                  yesterday at 11:21 PM

                                  I'm not really interested in building an interpreter, but the part about scalar out of order execution got me thinking. The opcode sequencing logic of an interpreter is inherently serial and an obvious bottleneck (step++; goto step->label; requires an add, then a fetch and then a jump, pretty ugly).

                                  Why not do the same thing the CPU does and fetch N jump addresses at once?

                                  Now the overhead is gone and you just need to figure out how to let the CPU fetch the chain of instructions that implement the opcodes.

                                  You simply copy the interpreter N times, store N opcode jump addresses in N registers and each interpreter copy is hardcoded to access its own register during the computed goto.

                                    • saagarjha

                                      today at 12:56 AM

                                      You run into the same problem a CPU does: if you have dependencies between the instructions, you can't execute ahead of time. Your processor has a bunch of hardware to efficiently resolve conflicts but your interpreter does not.