Skip to content
Surf Wiki
Save to docs
general/compiler-construction

From Surf Wiki (app.surf) — the open knowledge base

Tracing just-in-time compilation

Technique used to optimize the execution of a program at runtime


Summary

Technique used to optimize the execution of a program at runtime

Tracing just-in-time compilation is a technique used by virtual machines to optimize the execution of a program at runtime. This is done by recording a linear sequence of frequently executed operations, compiling them to native machine code and executing them. In contrast, traditional just-in-time compilation (JIT) compiles each method in turn, without optimizing between them.

Overview

Just-in-time compilation is a technique to increase execution speed of programs by compiling parts of a program to machine code at runtime. One way to categorize different JIT compilers is by their compilation scope. Whereas method-based JIT compilers translate one method at a time to machine code, tracing JITs use frequently executed loops as their unit of compilation. Tracing JITs are based on the behavior that many programs spend most of their time in some loops of the program, termed hot loops, and subsequent loop iterations often take similar paths. Virtual machines that have a tracing JIT are often mixed-mode execution environments, meaning that they have either an interpreter, or a method compiler, along with the tracing JIT.

Technical details

A tracing JIT compiler goes through various phases at runtime. First, profiling information for loops is collected. After a hot loop has been identified, a special tracing phase is entered, which records all executed operations of that loop. This sequence of operations is called a trace. The trace is then optimized and compiled to machine code. When this loop is executed again, the compiled trace is called instead of the program counterpart.

These steps are explained in detail in the following:

Profiling phase

The goal of profiling is to identify hot loops. This is often done by counting the number of iterations for every loop. After the count of a loop exceeds a certain threshold, the loop is considered to be hot, and tracing phase is entered.

Tracing phase

In the tracing phase the execution of the loop proceeds normally, but in addition every executed operation is recorded into a trace. The recorded operations are typically stored in a trace tree, often in an intermediate representation (IR). Tracing follows function calls, which leads to them being inlined into the trace. Tracing continues until the loop reaches its end and jumps back to the start.

Since the trace is recorded by following one concrete execution path of the loop, later executions of that trace can diverge from that path. To identify the places where that can happen, special guard instructions are inserted into the trace. One example for such a place are if statements. The guard is a quick check to determine whether the original condition is still true. If a guard fails, the execution of the trace is aborted.

Since tracing is done during execution, the trace can be made to contain runtime information (e.g., type information). This information can later be used in the optimization phase to increase code efficiency.

Optimization and code-generation phase

Traces are easy to optimize, since they represent only one execution path, which means that no control flow exists and needs no handling. Typical optimizations include common-subexpression elimination, dead code elimination, register allocation, invariant-code motion, constant folding, and escape analysis.{{cite conference |book-title=Proceedings of the 20th ACM SIGPLAN workshop on Partial evaluation and program manipulation |access-date=2020-12-13

After the optimization, the trace is turned into machine code. Similarly to optimization, this is easy due to the linear nature of traces.

Execution

After the trace has been compiled to machine code, it can be executed in subsequent iterations of the loop. Trace execution continues until a guard fails.

History

Whereas the idea of JITs reaches back to the 1960s, tracing JITs have become used more often only recently. The first mention of an idea that is similar to today's idea of tracing JITs was in 1970.{{cite thesis |access-date=2020-12-13

The first implementation of tracing is Dynamo, "a software dynamic optimization system that is capable of transparently improving the performance of a native instruction stream as it executes on the processor".{{cite conference |book-title=Proceedings of the ACM SIGPLAN 2000 conference on Programming language design and implementation |access-date=2020-12-13

Dynamo was later extended to DynamoRIO. One DynamoRIO-based project was a framework for interpreter construction that combines tracing and partial evaluation. It was used to "dynamically remove interpreter overhead from language implementations".{{cite conference |book-title=Proceedings of the 2003 workshop on Interpreters, virtual machines and emulators |access-date=2020-12-13

In 2006, HotpathVM, the first tracing JIT compiler for a high-level language was developed.{{cite conference |book-title=Proceedings of the 2nd international conference on Virtual execution environments |access-date=2020-12-13

Another example of a tracing JIT is TraceMonkey, one of Mozilla’s JavaScript implementations for Firefox (2009).{{cite conference |book-title=Proceedings of the 30th ACM SIGPLAN Conference on Programming Language Design and Implementation |access-date=2020-12-13

Another project that utilizes tracing JITs is PyPy. It enables the use of tracing JITs for language implementations that were written with PyPy's translation toolchain, thus improving the performance of any program that is executed using that interpreter. This is possible by tracing the interpreter itself, instead of the program that is executed by the interpreter.{{cite conference |book-title=Proceedings of the 4th workshop on the Implementation, Compilation, Optimization of Object-Oriented Languages and Programming Systems |access-date=2020-12-13

Tracing JITs have also been explored by Microsoft in the SPUR project for their Common Intermediate Language (CIL). SPUR is a generic tracer for CIL, which can also be used to trace through a JavaScript implementation.{{cite conference |book-title=Proceedings of the ACM international conference on Object oriented programming systems languages and applications |access-date=2020-12-13

Example of a trace

Consider the following Python program that computes a sum of squares of successive whole numbers until that sum exceeds 100000:

PYTHON
def square(x):
    return x * x

i = 0
y = 0
while True:
    y += square(i)
    if y > 100000:
        break
    i = i + 1

A trace for this program could look something like this:

PYTHON
 loopstart(i1, y1)
 i2 = int_mul(i1, i1)		# i*i
 y2 = int_add(y1, i2)		# y += i*i
 b1 = int_gt(y2, 100000)
 guard_false(b1)
 i3 = int_add(i1, 1)		# i = i+1
 jump(i3, y2)

Note how the function call to square is inlined into the trace and how the if statement is turned into a guard_false.

References

Wikipedia Source

This article was imported from Wikipedia and is available under the Creative Commons Attribution-ShareAlike 4.0 License. Content has been adapted to SurfDoc format. Original contributors can be found on the article history page.

Want to explore this topic further?

Ask Mako anything about Tracing just-in-time compilation — get instant answers, deeper analysis, and related topics.

Research with Mako

Free with your Surf account

Content sourced from Wikipedia, available under CC BY-SA 4.0.

This content may have been generated or modified by AI. CloudSurf Software LLC is not responsible for the accuracy, completeness, or reliability of AI-generated content. Always verify important information from primary sources.

Report