Description
A key part of the original design thesis for the JIT was that it was OK to jit small sections of code, provided that the cost of entering and exiting jjitted code was small enough.
Ideally, entering (and exiting) jitted code should cost no more than 2 or 3 instruction dispatches in the interpreter.
At the moment, we are nowhere near that.
To achieve that low overhead, we need transfers to perform minimal memory accesses and use reasonably easily predictable branches.
What we have now
ENTER_EXECUTOR
This is where code enters the jit. Currently this does an eval-breaker check (to avoid needing to perform an escaping call in the jit) then increfs the executor, to keep it alive, calls the shim frame, which then calls the actual jitted code.
_EXIT_TRACE
This is where jitted code transfers control back to the interpreter or to other jitted code.
This uop contains complex logic to determine whether the exit is "hot", calls the jit compiler or jumps to other jitted code. Even in the case where jitted code already exists, it still needs to check for validity, before making a doubly dependent load to find the jitted code: exit->executor->jitted_code
.
What we want:
First of all, the interpreter and jit need to use the same calling convention. We can do this by using the tailcalling interpreter and TOS caching, such that the jitted code and interpreter functions take the same parameters.
We also want to refactor the executor or calling conventions, to save an indirection. ie. exit->jit
rather than exit->executor->jit
.
ENTER_EXECUTOR
With the same calling convention, there should be no need for a shim frame.
So, apart from the eval-breaker check, ENTER_EXECUTOR
only needs to find the executor and make the tailcall executor->jit(...)
.
_EXIT_TRACE
By handling cold exits and invalid executors in stubs, _EXIT_TRACE
can avoid complex control flow and tailcall directly into the exit: exit->jit(...)
.