I have a language with a working interpreter, and I am trying to add just-in-time compilation (JIT) to it. For ease of implementation, only individual branches are compiled, while control flow structures are still handled by the interpreter. Unlike traditional JIT, I can't compile on a function level, since this is an esolang that doesn't have functions.
For performance reasons, I plan to have the JIT compile some branches on a background thread before they’re actually needed. Consider some code like the following:
a();
if (someCondition) {
b();
if (anotherCondition) {
c();
}
} else {
d();
}
e();
When a()
is hit, either branch b()
or d()
is next, so they should be prioritized over c()
and e()
. My initial thought for how to implement this was by storing each branch yet to be compiled in a priority queue, and use the following algorithm:
If the interpreter decides to execute a branch, and that branch is already compiled, run the compiled version of that branch. Otherwise, increase the priority of that branch by 1 (so that hot paths reach the front more quickly) and increase the priority of its following branches by 2 (so that branches that still have a chance to be compiled are prioritized over ones we already missed), and continue interpreting.
However, there’s a flaw in this algorithm. What if this happens?
a()
is hit and not yet compiled, so its priority is increased by 1, andb()
andd()
have their priorities increased by 2.b()
is compiled just asa()
finishes execution.- The condition ends up being true, and
b()
is executed.
Now, d()
has the highest priority while c()
and e()
have the lowest priority, which is the opposite of what we want. If I change the priority of branches following compiled ones, 100% compiled code will spend time searching the priority queue to adjust the priority of things that aren’t even there.
How do I choose what branches to compile next for maximum performance?