# Are Abstract-interpreter Baseline JITs Worth it? An Empirical Evaluation through Metacompilation

Nahuel Palumbo, Guillermo Polito, Stéphane Ducasse, Pablo Tesone

► To cite this version:

Nahuel Palumbo, Guillermo Polito, Stéphane Ducasse, Pablo Tesone. Are Abstract-interpreter Baseline JITs Worth it? An Empirical Evaluation through Metacompilation. CGO 2026 - IEEE/ACM International Symposium on Code Generation and Optimization, Jan 2026, Sydney, Australia. hal-05407834

HAL Id: hal-05407834

https://inria.hal.science/hal-05407834v1

Submitted on 9 Dec 2025

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_1.png" /></figure>

Distributed under a Creative Commons Attribution 4.0 International License
# Are Abstract-interpreter Baseline JITs Worth it?
## An Empirical Evaluation through Metacompilation

Accepted to CGO'26

1st Nahuel Palumbo

Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRISTAL
Lille, France

nahuel.palumbo@inria.fr

2nd Guillermo Polito

Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRISTAL
Lille, France

guillermo.polito@inria.fr

3rd Stéphane Ducasse

Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRISTAL
Lille, France

stephane.ducasse@inria.fr

4th Pablo Tesone

Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRISTAL
Lille, France

pablo.tesone@inria.fr

**Abstract**—Baseline JIT compilers need to compile early and as fast as possible, while still performing optimizations. One powerful technique to write fast baseline JIT compilers is abstract interpretation. Several implementations of this technique exist in practice, implementing in a single pass optimizations such as register allocation, constant propagation, and instruction scheduling. However, although they share the same technique, all these implementations vary in the exact optimizations performed, their internal design and further implementation details (e.g., the implementation language and framework). Thus, it is challenging to understand and isolate the benefits of the technique by simply studying these existing implementations.

Understanding the real impact of compile-time abstract interpreters requires isolating performance differences and experimenting with different variations of the *same* implementation, which demands extensive engineering work. In this paper, we propose to analyse the impact of abstract interpreters through metacompilation. We use metacompilation as a means to (a) reduce the experimentation effort and (b) to produce compiler variants that are comparable, reducing implementation noise.

We implemented our solution to generate several JIT compiler variants for the Pharo VM. We describe the adaptations required in the metacompilation framework to target both *abstract interpreters* and *direct translators*, in combination with *Static Type Prediction* optimizations.

Our benchmarks show that compile-time abstract interpreters, on average, reduce the emitted machine code size by 12% and increase execution speed by 10%, up to 30%, without increasing JIT compilation overhead, compared to direct translators.

***Index Terms***—JIT compiler, abstract interpreter, metacomplier, compiler optimization, Pharo.

## I. INTRODUCTION

Baseline JITs are required to achieve decent cold and startup performance. The key to baseline JIT compilation is to compile early and as fast as possible, while still performing optimizations like register allocation, constant propagation, and instruction scheduling. One powerful technique to implement *fast* baseline JITs is to use a compile-time abstract interpreter framework [1], [2]. Thus, in this paper we use the term *abstract-interpreter* to describe the compilation framework that performs single-pass stack-to-register translation

with peephole optimizations, aimed for fast compilation, as previously described by Titzer [3]. To illustrate how these compilers work, Figure 1 presents two versions of compiling the expression `return temp0 + 1` from a stack-based bytecode: direct translators and abstract interpreters. By using abstract interpreters, operations accessing the stack are avoided by keeping the values in registers.

<figure><img src="image_2.png" /><figcaption>Fig. 1: JIT compiled machine code for the expression `return temp0 + 1` using direct translators and abstract interpreters. For a direct translator, it emits as many instructions as defined by the bytecode method. Using abstract interpreters, instructions accessing the stack are avoided (*peephole optimization* in a single pass).</figcaption></figure>

To perform these optimizations, abstract interpreters stage stack operations on the abstract state (*abstract stack* for the rest of the paper) to compile time. That imposes a different style of code to the baseline JIT writers, where many decisions need to be staged, either manually or automatically by a framework. In previous work, Titzer [3] studied several existing baseline implementations, identifying common design traits and optimizations applied in their implementations. Moreover, the author performed a thorough performance evaluation to compare such implementations. However, such a comparison
does not allow us to clearly understand the impact of each
design decision: optimization differences are intertwined, and
implementation details such as the chosen compilation frame-
work may introduce undesired noise in the measurements. To
understand the performance impact of the technique, we need
to isolate the studied traits by producing different variations
of the same implementation. Of course, such an experimental
approach demands extensive engineering work.

In this paper, we propose an experimental methodology
that solves these issues by metacompilation [4]–[7]. Using
metacompilation in our experimental design, we solve the two
issues described above. First, metacompilation can be used to
produce semantically equivalent variants of the same compiler,
reducing implementation noise. Second, metacompilation re-
duces the experimentation effort by generating JIT compiler
frontends from the same interpreter definition.

We extend the Druid metaccompiler [8] to generate several
versions of the Pharo Virtual Machine baseline JIT compiler to
obtain semantically equivalent compilers and concentrate on
their optimizations: abstract interpretation and ahead-of-time
staged *Static Type Prediction* optimizations [9]. We evaluated
the different versions of the generated VMs by running 20
different benchmark programs. The results show, on average
(geomean), a reduction of 12% of emitted instructions, up
to 26%, by targeting abstract interpreters. Our measurements
show that targeting abstract interpreters increases the speed-up
of the benchmarks by 10%, on average (geomean), up to 30%,
over direct translators while preserving similar JIT compilation
overhead. Moreover, *Static Type Prediction* shows a speed-up
of up to 7% on top of it.

In previous work, Palumbo et al. explored the design of
a baseline JIT metaccompiler using Druid [8], comparing the
differences between metacompiled vs handwritten baseline JIT
compiler frontends. In this paper, instead, metacompilation
is used as a means to understand the impact of baseline
JIT compiler optimizations. In this work, we extended the
metacompilation approach to target abstract interpreters. Con-
cretely, the contributions of this paper are:

*   **A rigorous evaluation of abstract-interpreter for baseline JIT Compilation through metacompilation.** We evaluate the benefits of JIT compilation by comparing two equivalent compiler implementations obtained through metacompilation.

*   **Empirical evidence of the benefits of abstract-interpreters for baseline JIT Compilation.** Our rigorous evaluation shows that abstract interpreters are indeed profitable: they reduce compiled code size and improve run time while not incurring significant compilation time.

*   **Code transformations to metacompile abstract interpreters.** We describe a set of transformations implemented in the metac乐编译器 to target JIT compilers based on abstract interpretation to preserve the semantics along the required staged decisions.

*   **A real use case about metacompilation of *Static Type Prediction*.** We use the transformations described in this paper to metacompile the *Static Type Prediction*

optimizations implemented in the interpreter. We measure
the impact of these optimizations combined with abstract
interpretation on baseline JIT compiler.

II. COMPILING WITH ABSTRACT INTERPRETERS

This section presents the terminology and concepts used in
the rest of the paper. We first introduce the language targeted
by the metac乐编译器 based on abstract interpretation. We then
present an example of how abstract interpretation is used
for compilation. Finally, this section introduce the idea of
metacompilation of baseline JIT compilers and present our
research questions.

A. Stack-based Abstract Interpreter Register Transfer Lan-
guage

Compilation can be addressed as an abstract interpretation
problem. This section presents such a compilation approach
through a stack-based abstract machine. The machine receives
as input programs represented as a list of stack operations. The
output is assembly-like code for a register machine. The goal
of the machine is to perform peephole optimizations during
the code generation step, avoiding a separate optimization
pass through e.g., pattern matching. Such a single-pass code
generation approach is profitable, for example, for baseline JIT
compilers that look for low-overhead compilation.

For presentation purposes, this section focuses on a simple
machine explaining the core semantics. A production-ready
abstract machine, as we will describe later in Section III, will
also be sensitive to control flow i.e., abstract states should be
tracked at control flow branch points and merged in control
flow merge points [10], [11]. The input programs of the
machine are expressed by the syntax in Figure 2. Our input
language looks like a normal stack machine. It manipulates
two kinds of values: registers (r) and numeric constants
(c). Input programs are composed of concrete assembly-like
instructions (in uppercase, e.g., Ret and Move), and stack
instructions (prefixed by abs, e.g., absPush and absPop)
that manipulate the abstract stack.

<figure><img src="image_3.png" /><figcaption>Fig. 2: Syntax.</figcaption></figure>

The abstract machine is a three-tuple containing a list of
input instructions, an abstract stack, and a list of generated
instructions. Figure 3 shows the reductions of such a machine
for each of the input instructions defined in Figure 2. We use
tuple notation and pattern matching to represent the instruction
list and the stack.
$$ P \vdash \langle\langle\langle\text{Ret}, \mathit{Is}\rangle, \text{Stack}, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \text{Stack}, \langle\langle\mathit{Os}, \text{Ret}\rangle\rangle \rangle \quad [\text{genRet}] $$

$$ P \vdash \langle\langle\langle\text{Push}(v), \mathit{Is}\rangle, \text{Stack}, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \text{Stack}, \langle\langle\mathit{Os}, \text{Push}(v)\rangle\rangle \rangle \quad [\text{genPush}] $$

$$ P \vdash \langle\langle\langle\text{Pop}(r), \mathit{Is}\rangle, \text{Stack}, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \text{Stack}, \langle\langle\mathit{Os}, \text{Pop}(r)\rangle\rangle \rangle \quad [\text{genPop}] $$

$$ P \vdash \langle\langle\langle\text{Mov}(v, r), \mathit{Is}\rangle, \text{Stack}, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \text{Stack}, \langle\langle\mathit{Os}, \text{Mov}(v, r)\rangle\rangle \rangle \quad [\text{genPop}] $$

$$ P \vdash \langle\langle\langle\text{absPush}(v), \mathit{Is}\rangle, \text{Stack}, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \langle\langle\mathit{v}, \text{Stack}\rangle\rangle, \mathit{Os}\rangle \quad [\text{abs\_push}] $$

$$ P \vdash \langle\langle\langle\text{absPop}(r), \mathit{Is}\rangle, \langle\langle\text{top}, \text{Tail}\rangle, \mathit{Os}\rangle \\ \qquad \to \langle\langle \mathit{Is}, \text{Tail}, \langle\langle\mathit{Os}, \text{Mov}(\text{top}, r)\rangle\rangle \rangle \quad [\text{abs\_pop}] $$

<caption>Fig. 3: Reductions</caption>

The first four rules in Figure 3 show the reduction of concrete instructions by producing the same instruction in the output instruction list. Extending the model for other kinds of concrete instructions is trivial and follows the same pattern. The last two rules show the code behavior of the abstract interpreter: `absPush` does not modify the instruction list. Instead, it pushes the value to the stack of the abstract machine. Conversely, the `absPop` instruction takes the value at the top of the stack and generates a `Move` instruction.

This machine halts if an `absPop` happens on an empty stack. It is outside of the scope of this article to guarantee or verify that input programs have the correct/intended semantics. However, given these machine semantics, we define whether an input program is well-formed or not. This property is important for the rest of the article: all input programs in this article must be well-formed, and a metacompile generating such input programs should ensure this property.

**Well-formedness:** We say an input program is *well-formed* if its execution does not halt and the final machine's stack is empty.

## B. Compiling with Abstract Interpreters by Example

To illustrate how such an abstract compiler is used in practice, let us consider an example of stack bytecode to machine code translation. The compiler is built as an abstract interpreter of bytecode, where each opcode is associated with a *handler function* responsible for generating the intermediate representation instructions. In our example abstract machine, such an intermediate representation (IR) is created by explicitly appending instructions into an output stream and manipulating an abstract stack (Section II-A).

Figure 4 presents a more detailed example of translating the expression `return temp0 + 1`. The bytecode instruction representation of the code is shown in the left column. In the second column, each bytecode is associated with a handler function that generates the IR implementing its semantics. The third column shows the state of the abstract stack after

executing the handler. Finally, the fourth column shows the generated code after each handler.

This example shows how abstract interpreter-based compilers perform intra-procedural peephole optimizations between bytecode instructions. Stack accesses modify the abstract machine, and instructions are only emitted when the abstract state is used. Overall, this avoids emitting instructions that access the stack at runtime. Moreover, it is important to notice that this technique works as a single forward pass during code generation (in this case, IR generation), thus not requiring extra compilation passes.

This makes this technique an interesting target for baseline JITs that require low-overhead compilation times.

## C. Static Type Predictions

In addition to abstract interpretation, a common optimization for virtual machines is *Static Type Prediction* [9]. *Static Type Prediction* is a message-send optimization that predicts the types of the receiver and arguments based on the message selector. The key idea of this optimization is to statically inline common and predictable sends into a fast path guarded by type checks. If the guard fails, execution falls back to the slow path, which performs a normal message send.

For example, the Pharo VM applies this optimization to common integer operations such as arithmetic, comparisons, and bitwise manipulations. For instance, although developers can redefine the addition operator `+` in their own classes, most call sites will only observe integers at run time. Thus, the bytecode compiler emits a special *Static Type Prediction*-optimized bytecode for addition, illustrated with a simplified version of the interpreter code in Figure 5 (a full version of this bytecode handler is presented in Appendix VI).

The instruction handler implements two execution paths: a fast path guarded by type checks on both operands, and a slow path that performs a regular message send. If an overflow occurs, the semantics require delegating to user code, so the slow path is taken in that case as well.

Implementing *Static Type Prediction* optimizations in an abstract interpreter baseline JIT compiler introduces further challenges, because it requires a careful manipulation of the abstract stack, as explained in Section III-B.

## D. Research Questions

Abstract interpreters compilation frameworks are present in baseline JIT compilers for languages such as WebAssembly [3] and Pharo [12]. In previous work, Titzer compared different implementations of abstract interpreter-based compilers for WebAssembly, shedding light on the behavior of such compilers. However, using different implementations introduces potential biases in the measurements.

In this article, we explore the benefits of abstract interpreter-based compilers through metacompile. We investigate the benefits and drawbacks of such compilers by metacompiling several compilers with different features from the same interpreter: with and without abstract interpretation and static type prediction.
<figure><img src="image_4.png" /><figcaption>Fig. 4: JIT compilation of a bytecode method based on abstract interpreters for the code `return temp0 + 1`. The abstract interpreter resolves stack operations at compile-time and generates an optimized IR. The emitted JITted machine code does not use the stack at runtime.</figcaption></figure>

```
1 Interpreter >> bytecodeAdd
2 | rcvr arg result |
3 rcvr := self stackValue: 1.
4 arg := self stackValue: 0.
5 "Type check both operands"
6 (objectMemory areIntegers: rcvr and: arg) ifTrue: [
7     result := (objectMemorychtementOf: rcvr)
8       + (objectMemorychtementOf: arg).
9
10 "Type check the result.
11 If overflow, roll back to the slow path"
12 (objectMemory isIntegerValue: result) ifTrue: [
13     self pop: 2
14       thenPush: (objectMemory integerObjectOf: result).
15     ^ self fetchNextBytecode ]].
16
17 "Slow path: send"
18 messageSelector := self specialSelector: 0.
19 argumentCount := 1.
20 self normalSend
```

<caption>Fig. 5: Bytecode handler for addition with *Static Type Prediction* for small integers.</caption>

This work studies the following research questions:

- **RQ1:** Does abstract interpretation reduce code size?

- **RQ2:** Does abstract interpretation improve performance?

- **RQ3:** Does abstract interpretation introduce JIT compile time overhead?

### III. METACOMPILATION OF BASELINE JIT COMPILERS

In this work, we use a metacompilation approach to generate the different JIT compiler versions for the same VM and interpreter (Section IV). This section presents an overview of the metacode, called Druid, and how JIT compilers with different features are generated. To illustrate the issues and

the upcoming solutions, we will use excerpts of the Pharo Virtual Machine and its JIT compiler named Cogit [12], our experimental platform (Section IV-A).

#### A. Running Example

We illustrate the metacompilation of baseline JITs with an example: a push array bytecode instruction (Figure 6) taken from the Pharo Virtual Machine. This bytecode pops the top N elements from the runtime stack into a new array and pushes this resulting array to the stack. On the interpreter implementation (left of the figure), lines 3-8 allocate a new array of size N, lines 10-12 loop, popping each element from the stack and storing it into the array, and finally, line 14 pushes the array to the stack. It's important to notice that the Pharo VM implements a generational scavenger [13] and thus has two ways to allocate objects: a fast path using a bump allocator, and a slow path on a larger memory managed using a free tree.

The right side of Figure 6 shows the corresponding bytecode handler on the baseline JIT compiler. This version of the same bytecode shows that the JIT compiler frontend is written in a more intricate style. The instructions marked with the prefix `gen` generate code to be executed at runtime. The loop is staged and executed at JIT compile time. Finally, because of compile time and run time pressure, this bytecode compiles only the fast allocation path and delegates the slow paths to a runtime service. Due to the possible runtime call, the abstract stack needs to be spilled to memory.
1 Interpreter >> pushNewArrayBytecode
2 | size array |
3 size := self fetchByte.
4 (self fitsInNewSpace: size)
5 ifTrue: [
6   array := self fastInstantiateArray: size ]
7 ifFalse: [
8   array := self slowInstantiateArray: size ].
9
10 0 to: size - 1 do: [ :i | | element |
11 element := self popStack.
12 array at: size - i - 1 put: element ].
13
14 self push: array

JITFrontend >> gen_pushNewArrayBytecode
| size r1 |
size := self fetchByte.

"Runtime call requires spilling the stack"
self absStackSpill.
(self gen_fitsInNewSpace: size)

ifTrue: [
  r1 := self gen_fastInstantiateArray: size ]

ifFalse: [
  r1 := self gen_runtimeCall: #slowInstantiateArray:
    with: size ].

"Merge point: abstract states must not conflict"
self absPush: r1

<caption>Fig. 6: Bytecode for creating a new array with N elements pushed to the stack. Implementations for the Interpreter handler (left) and JIT compiler generator handler (right).</caption>

## B. Challenges: Abstract-Interpreter Compiler Invariants

The goal of a baseline JIT metacompile is to ahead-of-time generate, from the interpreter implementation, a semantically equivalent JIT compiler frontend implementation. The previous example illustrated that a correct translation needs to preserve the invariants imposed by the abstract interpreter:

* **Sync abstract states at merge points:** The abstract state from different branches must not conflict at merge points. That is, the same stack slot should not have two different abstract values.

* **Spilling at runtime interactions:** When the compiled code performs a runtime call, the abstract stack must be spilled to the runtime stack in memory to make that state accessible by runtime services.

* **Sync the abstract stack and runtime stack:** Stack effects should affect the abstract stack in the same order as they would affect the runtime stack i.e., perform the same number of `push/pops`, in the same order.

## C. The Druid metacompile in a Nutshell

Druid<fnref n="1" /> is a source-to-source ahead-of-time automatic approach to generate a baseline JIT compiler from a language interpreter [8]. Conceptually, Druid works as a compiler generator program (Cogen) [14].

In essence, Druid is engineered as an optimizing compiler with custom passes to target the Pharo VM JIT frontend. The Druid IR is a control flow graph (CFG) in three-address-code SSA form [15]. The unit of metacompile is a *bytecode handler i.e.*, given an interpreter handler, it generates the corresponding *generator handler*, as illustrated in Figure 7.

To enable our experimentation, we extended the Druid metacompile with several transformation passes required to target abstract interpreters: coalescing stack instructions, hoisting

<figure><img src="image_5.png" /><figcaption>Fig. 7: JIT compiler generation approach by metacompile of the interpreter.</figcaption></figure>

stack spill instructions, and carefully managing runtime control flow, particularly in the presence of abstract stack instructions.

## D. Abstract Stack Instruction Coalescing

To avoid the divergence of abstract states on different branches, we coalesce all stack instructions to the *closest critical post-dominator basic block*. This pass identifies all stack instructions in branches and uses a post-dominator tree to merge them into a single instruction. Figure 8 presents this transformation. It assigns the pushed values to SSA registers, generates a new *phi function* [15] of all those values in the closest critical post-dominator basic block, and pushes the *phi function* value.

This transformation makes sure that effects on the abstract stack happen only once, removing potential conflicts by construction.

## E. Spill Hoisting

Before a runtime interaction from the compiled code, it is necessary to spill all the values from the abstract to the runtime stack. However, if this spill is done inside a non-staged branch,

<footnote n="1">¹https://github.com/Anonimized-for-double-blind visited on 2025-03-01.</footnote>
<figure><img src="image_6.png" /><figcaption>Fig. 8: Coalescing pass for stack instructions on branches.</figcaption></figure>

we must ensure that the abstract state is spilled across all branches arriving at the subsequent merge point.

To avoid this conflict in abstract states, we hoist and merge, if necessary, all spill instructions to the *closest critical dominator basic block* to make the synchronization safe, as shown in Figure 9.

<figure><img src="image_7.png" /><figcaption>Fig. 9: Forcing spilling of the *abstract stack* to the runtime. The hoisting pass synchronizes the stacks for all branches.</figcaption></figure>

## F. Scheduling and Stack Dependencies

Staged stack instructions should have the same observable effect as non-staged compiled code. To guarantee this, the metac compiler tracks stack-effect dependencies between stack instructions from the interpreter (Figure 10) and then schedules basic blocks based on these dependencies. Informally speaking, staged *Push* and *Pop* instructions will be executed in the same order as they would be in runtime.

Moreover, since all abstract stack operations are staged and executed at JIT compile-time, when the abstract stack is modified in a loop, the loop itself needs to be staged too. A loop's body is then interpreted N times (unrolled) and the abstract stack is synchronized with the real one at the exit point.

The metac compiler stages all non-runtime dependent instructions to jit compile time [16]. If the loop cannot be staged (because it depends on runtime information), the abstract interpreter cannot correctly compute the abstract state at the end of the loop. This forbids metacompilation, and a compile-time error must be raised.

<figure><img src="image_8.png" /><figcaption>Fig. 10: Stack instruction dependencies based on interpreter code.</figcaption></figure>

# IV. EXPERIMENTATION

To study the impact of abstract interpreter baseline JIT compilers, we used Druid to metacompile different JIT compiler versions for the Pharo programming language. For each JIT compiler version, Druid generates the bytecode handlers from the interpreter definition. This section presents in detail the experimental platform, our experimental design, and our results.

## A. Experimental Platform: the Pharo VM JIT Compiler

For practical purposes, we choose in this article to experiment on top of the Pharo VM, targeting metacompilation to the Cogit JIT compiler framework [12]. The Pharo Virtual Machine has been in production for over 20 years [17]. The Pharo VM implements an indirect-threaded stack bytecode interpreter and the Cogit framework for JIT compilation, which has been in production for more than 10 years [12].

Cogit frontends can be implemented in either a direct style or using an abstract stack. Moreover, the Pharo interpreter implements *Static Type Prediction* optimizations.

The Cogit JIT compiler framework organizes compilers into frontend and backend components. Cogit frontends are organized as bytecode handlers defining the low-level translation semantics of bytecode to an intermediate language called *CogRTL*, similarly to the presented in Section II-A. The *CogRTL* intermediate language defines a two-address-code Register Translate Language (RTL) with fixed virtual registers. Thus, each handler (1) generates a sequence of low-level instructions in the *CogRTL* language and (2) modifies the abstract stack. Finally, the Cogit JIT backends then generate machine code from a given CogRTL input program.

## B. Experimental Design Overview

To answer our research questions, we propose to metacompile different JIT compiler versions of the Pharo VM and compare them in (a) performance, (b) generated code size and (c) JIT compile-time.

We considered the following variations.

**Direct translation:** Handlers emit CogRTL instructions directly without using the abstract stack. Data is shared between different bytecode instructions by emitting push/pop instructions that manipulate the runtime stack.
**Abstract stack translation:** Handlers mix CogRTL instructions with abstract stack manipulations. Data is shared between different bytecode instructions by pushing/popping values to the abstract stack. Instructions accessing the runtime stack are generated automatically if needed e.g., stack spilling when calling a runtime service such as garbage collection.

a) *Benchmark Configurations:* We considered JIT compiler frontends in direct style, abstract interpreter, with and without *Static Type Prediction*. Our comparison baseline is the threaded interpreter used for metacompilation. This leaves us with 5 VM variants with equivalent semantics:

*   **ONLY INTERPRETER.** A Pharo VM without JIT compilation. It is used as a baseline for execution speed.

*   **JIT ABSTRACT.** Pharo VM using a baseline JIT with Abstract Interpretation, performing abstract stack optimizations to avoid accessors to the stack at runtime.

*   **JIT DIRECT.** Pharo VM using a baseline JIT with Direct Translators, emitting all instructions declared by the JIT compiler.

*   **JIT ABSTRACT WITH STATIC TYPE PREDICTION.** Same as JIT ABSTRACT with Static Type Prediction optimization for special messages.

*   **JIT DIRECT WITH STATIC TYPE PREDICTION.** Same as JIT DIRECT with Static Type Prediction optimization for special messages.

b) *Benchmark Programs:* We use each VM to run a set of well-known benchmark programs and a set of macro benchmarks, described in Appendix VI, using the Rebench framework [18], [19]. We run each benchmark using 30 VM iterations. Micro benchmarks use 10 in-process with two (ignored) warmup iterations. This number of iterations showed enough to measure the behavior of the baseline JIT compilers. We use the test suites of several Pharo packages as macro benchmarks. Macro benchmarks use a single in-process iteration.

c) *Hardware Configuration:* We build each VM and run the benchmarks using a Linux Debian 5.10.140-1 distribution on a Intel(R) Xeon(R) CPU E5620 @ 2.40GHz processor with 16 GB DDR3 of RAM.

### C. RQ1: Does abstract interpretation reduce code size?

Figure 11 presents the emitted instruction size by JIT compilation for each configuration per benchmark. Values are in MB. Lower is better. The first two bars describe JIT ABSTRACT and JIT DIRECT, respectively. The next two are the same, but include *Static Type Prediction*.

Abstract interpretation reduces the size of emitted instructions for every benchmark compared to direct translations. There are 12% fewer emitted instructions, and 11% with *Static Type Prediction*, using a geometric mean. MICRODOWN has the best result with 26% on emitted instruction reduction, meanwhile the worst case is BENCHREGEXDNA with ~5% of reduction.

<figure><img src="image_9.png" /></figure>

Ab = JIT ABSTRACT; Di = JIT DIRECT; \*STP = with Static Type Prediction

<caption>Fig. 11: Emitted code size by JIT compilation (MB). Lower is better.</caption>

**RQ1 Conclusion.** Targeting JIT compilation to abstract interpreters reduces the emitted machine code size by 12%, using a geometric mean, up to 26%. All our benchmarks showed a reduction in the emitted machine code size, being ~5% the worst case. Using *Static Type Prediction* on top of abstract interpretation further increases the emitted instruction size by 1%.

### D. RQ2: Does abstract interpretation improve performance?

Figure 12 presents the speed-up of each configuration relative to ONLY INTERPRETER runtime, per benchmark. Higher is better. Benchmark results reveal that JIT DIRECT executes, on geometric mean, 1.7x faster, up to 4.7x. JIT ABSTRACT is 1.8x faster, up to 5.1x. Thus, JIT ABSTRACT increases by 10%, up to 30%, the speed of JIT DIRECT.

We observe some negative results. For the *Fasta* benchmark, using JIT compilation is slower than ONLY INTERPRETER. Moreover, JIT ABSTRACT is 5% slower than JIT DIRECT for *PiDigits*. These benchmarks already presented negative results for JIT compilation in [8].

Applying *Static Type Prediction* optimizations, the execution speed increases 2%, up to 7%, for JIT ABSTRACT. The optimization has more effect in JIT DIRECT, with a speed-up
<figure><img src="image_10.png" /></figure>

Ab = JIT ABSTRACT; Di = JIT DIRECT; \*STP = with Static Type Prediction

<figure><img src="image_11.png" /><figcaption>Fig. 13: Ratio of JIT compilation time relative to total execution time. Lower is better.</figcaption></figure>

Ab = JIT ABSTRACT; Di = JIT DIRECT; \*STP = with Static Type Prediction

of 4%, up to 30%. In both cases, the worst case is 5% slower.

**RQ2 Conclusion.** Targetting JIT compilation to abstract interpreters increases the execution speed by 10%, up to 30%. JIT compilers performing *Static Type Prediction* optimizations further increase the execution speed by 2%, up to 7%. The performance of some benchmarks is not sensitive to JIT compilation, presenting a slowdown between 5% and 20% compared with ONLY INTERPRETER.

*E. RQ3: Does abstract interpretation introduce JIT compile time overhead?*

We measure the time that the JIT compiler is running to compute the impact on the overhead of our solution. Figure 13 shows the ratio of JIT compile time over the total execution time, per benchmark. Lower is better.

Most of the benchmarks present a JIT compilation overhead less than or equal to 1%. The overhead on MICRODOWN is ~10%, being the case with the most overhead, probably due to a *pathological case* [8]. STARTUP and COMPILER present an overhead of 5%.

Our results do not show significant differences between the configurations. The biggest differences appeared in the

MICRODOWN benchmark, where JIT DIRECT increases the overhead by 1% of the total execution time over JIT ABSTRACT, and using *Static Type Prediction* increases another 1%.

**RQ3 Conclusion.** Abstract interpreters do not present a significant impact on JIT compilation overhead. The geometric mean of all compilation overheads is below 1%. One benchmark represents the worst case of 10% overhead due to a *compilation pathological case*. Targeting JIT compilation to abstract interpreters does not show significant differences in this overhead, nor does it perform *Static Type Prediction*.

*F. Threats to Validity*

a) *Internal Validity.*: We did our best to select a set of diverse benchmark programs: from micro benchmarks to macro benchmarks taken from real-world libraries (i.e., MICRODOWN, FILE, and COMPILER). The benchmarks that did not appear sensitive to the optimizations we evaluated were not sensitive to JIT compilation in general, thus lowering our averages. Moreover, all benchmarks sensitive to JIT compilation were also sensitive to the abstract-interpretation and *Static Type Prediction* optimizations.
b) *External Validity.*: We believe that our results will be comparable with those of programming languages with similar instruction sets. The abstract interpretation technique evaluated in this paper performs peephole optimizations, which means that it will show improvements for stack-based languages compiled to many simple instructions, thus finding many opportunities for peephole optimization, in contrast to a few complex instructions [20], [21]. We experimented on the Pharo programming language, whose bytecode instruction set includes lots of small instructions that manipulate the stack [22].

## V. RELATED WORK

The history of JIT compilation [23] started with LISP [24] and improved by people from Smalltalk [25] and SELF [26]. JIT compilation has been explored in numerous previous works [1], [27], [28]. Implementations exist for Java [2], [29], [30], JavaScript [31], [32], Python [33], and CIL [34].

Abstract interpretation techniques were formalized by Cousot & Cousot for static program analysis and program transformations [10], [11].

### A. Comparing Compiler Implementations

Evaluating the compiler's performance is a hard task due to the non-deterministic VM components (e.g., Garbage Collection, multithread execution, caches) [35]. Moreover, comparing different compiler implementations makes the task harder. The design of benchmarks, the way to write the programs, and how the compiler's optimizations are implemented have an important impact on the results.

In this work, we reduce those errors by using the same language, the same VM, and the same JIT compiler generator to run 20 benchmarks. However, other researchers have explored approaches to comparing languages and/or compiler implementations.

a) *Comparing different languages on different VMs:* The Computer Language Benchmarks Game<fnref n="2" /> (CLBG) is a well-known project to compare the performance of different languages based on many benchmarks (many of them included in this work). Stefan Marr et al., studied the way to compare cross-language compiler benchmarks [19], including Java, JavaScript, Ruby, and other language implementations based on the Graal Compiler. Both approaches are based on similar benchmark suites but differ in how the programs are written (idiomatic or not) and how to measure the time (total execution or just steady-state).

b) *Comparing different compilers of the same language:* Titzer [3] compared different WebAssembly baseline compilers [36] with different features, including compile-time abstract interpretation and the optimizations present in this work. He measures the impact of different features in the baseline compilers and categorizes them in a speed-quality space. Such a comparison is sensitive to JIT compiler implementation details: two VMs developed by different people in different

technologies could present different speed-ups, even if they implement the same optimizations. For example, Titzer shows that V8 and Spidermonkey VMs, both written the same year (2018), in the same language (C++) and implementing the same optimizations (MR K ISEL MAP MV), show different results (Fig. 7 and 8). The difference between our work and Titzer's is that we compare (using metacompilation) a baseline JIT based on abstract interpretation against a non-abstract interpreter, with the objective of removing such implementation noise. Our goal is to isolate the compilation using abstract interpretation and to provide a fair comparison between JIT compiler variants.

c) *Comparing different languages on the same VM:* Other works present how many dynamic languages run on top of the Java JVM [37], [38]. They use the CLBG benchmark suite, and other real-world applications to measure how the features and optimizations inside the JVM affect the execution of other dynamic languages, i.e., Scala, Closure, Python, and Ruby.

### B. JIT Compiler Generators

In the last decades, some projects based on JIT compiler generation have become popular, some of them, including abstract interpretation for program analysis and optimizations.

a) *Pypy and Truffle:* The PyPy project generates a JIT Compiler from an interpreter written in RPython and based on meta-tracing [5], [39], [40]. It is used not only to develop a Python VM but also for other languages [19], [41], [42]. Truffle is a language implementation framework written in Java on top of the Graal Compiler [4], [43], [44]. It is based on a self-optimizing AST interpreter and JIT compilation via partial evaluation. Many language implementations are using this approach [19], [45]–[47].

Both Truffle and Pypy are engineered to generate optimizing JIT compilers. In contrast, our goal is to evaluate the abstract interpretation technique on baseline JIT compilers. Druid knows which expressions in the interpreter code are computable at JIT compile time, thus generating staged optimizations. The code generated by Druid is installed with the remaining VM code, allowing developers to read, modify, or mix generated and handwritten code before the VM deployment.

b) *Targeting machine code generator framework:* Similar to Cogit, the target framework of metacompilation presented in this work, there exist other dynamic optimization systems targeting JIT compilation, such as Dynamo [48] and VCODE [49].

### C. Abstract Interpreters Generators

Lim & Reps create the Transformer Specification Language (TSL) [50], an abstract-transformer-generator generator used for automatically creating different abstract interpreters for machine-code instruction sets.

Boucher & Feeley propose to perform static analysis of programs based on abstract compilation [51]. Instead of applying abstract interpreters to programs, they compile the

<footnote n="2">²https://benchmarksgame-team.pages.debian.net/benchmarksgame</footnote>
target program into an *analysis program* which computes the
desired analysis when it is executed.

Cousot *et al.*, propose an idea about performing abstract
interpretation over abstract interpreters, i.e., meta-abstract in-
terpretation [52]. They use this *analysis of the analyzer* to
speed up program analyses.

VI. CONCLUSION

In this paper, we explored the benefits of abstract interpre-
tation in baseline JIT compilers. We use a metacompilation
technique for targeting existing compile-time abstract inter-
preters and compare its performance over direct translators. We
extended the Druid metactrlanger to generate, from the stack
bytecode interpreter, several baseline JIT compiler variants for
the Pharo Virtual Machine, including compile-time abstract
interpreters and *Static Type Prediction* optimizations. Thus,
using the same metactrlanger and same interpreter definitions,
we generate comparable variants of JIT compilers, reducing
implementation noise and experimentation efforts.

We described the adaptations required in the metacompila-
tion framework to target abstract interpreters and interpreter
optimizations such as *Static Type Prediction*. To evaluate our
work, we generated many versions of the Pharo VM using
different baseline JIT compilers with different configurations,
all of them metacompiled by Druid from the same Pharo
interpreter. Then we measured the emitted instructions size
(jitted code), execution time, and JIT compiler overhead to
compare the implementations.

Our benchmarks reveal that baseline JIT compilers using
compile-time abstract interpreters reduce the emitted machine
code size by 12% on average (geomean), and increase the
execution speed up to 30%, without harming JIT compilation
overhead, over direct translators. On top of this, *Static Type
Prediction* optimizations increase the emitted instruction size
by an extra 1% while improving execution speed by an
additional 2% on average (geomean), up to 7%.

ACKNOWLEDGMENT

This work was funded by Inria's Action Exploratoire
AlaMVic.

REFERENCES

[1] D. Lesbre and M. Lemerre, "Compiling with abstract interpretation," *Proc. ACM Program. Lang.*, vol. 8, no. PLDI, Jun. 2024. [Online]. Available: https://doi.org/10.1145/3656392

[2] M. G. Burke, J.-D. Choi, S. Fink, D. Grove, M. Hind, V. Sarkar, M. J. Serrano, V. C. Sreedhar, H. Srinivasan, and J. Whaley, "The jalapeño dynamic optimizing compiler for java," in *Proceedings of the ACM 1999 Conference on Java Grande*, ser. *JAVA '99*. New York, NY, USA: Association for Computing Machinery, 1999, p. 129141. [Online]. Available: https://doi.org/10.1145/304065.304113

[3] B. L. Titzer, "Whose baseline compiler is it anyway?" in 2024 IEEE/ACM International Symposium on Code Generation and Optimization (CGO). Los Alamitos, CA, USA: IEEE Computer Society, mar 2024, pp. 207-220. [Online]. Available: https://doi.ieeecomputersociety.org/10.1109/CGO57630.2024.1044855

[4] T. Würthinger, C. Wimmer, A. Wöß, L. Stadler, G. Duboscq, C. Humer, G. Richards, D. Simon, and M. Wolczko, "One vm to rule them all," in *International Symposium on New Ideas, New Paradigms, and Reflections on Programming & Software (ONWARD'13)*, 2013. [Online]. Available: https://doi.org/10.1145/2509578.2509581

[5] C. F. Bolz, A. Cuni, M. Fijalkowski, and A. Rigo, "Tracing the meta-level: Pypy's tracing jit compiler," in *ICOOOLPS '09: Proceedings of the 4th workshop on the Implementation, Compilation, Optimization of Object-Oriented Languages and Programming Systems*. New York, NY, USA: ACM, 2009, pp. 18-25. [Online]. Available: https://doi.org/10.1145/1565824.1565827

[6] A. Yermolovich, C. Wimmer, and M. Franz, "Optimization of dynamic languages using hierarchical layering of virtual machines," *SIGPLAN Not.*, vol. 44, no. 12, p. 7988, oct 2009. [Online]. Available: https://doi.org/10.1145/1837513.1640147

[7] M. Thakur and V. K. Nandivada, “Pye: A framework for precise-yet-efficient just-in-time analyses for java programs,” *ACM Trans. Program. Lang. Syst.*, vol. 41, no. 3, jul 2019. [Online]. Available: https://doi.org/10.1145/3337794

[8] N. Palumbo, G. Polito, S. Ducasse, and P. Tesone, "Meta-compilation of baseline jit compilers with druid," *The Art, Science, and Engineering of Programming*, vol. 10, no. 1, Feb. 2025. [Online]. Available: http://dx.doi.org/10.22152/programming-journal.org/2025/10/9

[9] U. Holzle, “Adaptive optimization for self: Reconciling high performance with exploratory programming,” Ph.D. dissertation, Stanford University, Stanford, CA, USA, 1994.

[10] P. Cousot and R. Cousot, "Abstract interpretation: a unified lattice model for static analysis of programs by construction or approximation of fixpoints," in *Proceedings of the 4th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages*, ser. POPL '77. New York, NY, USA: Association for Computing Machinery, 1977, p. 238252. [Online]. Available: https://doi.org/10.1145/512950.512973

[11] —, "Systematic design of program transformation frameworks by abstract interpretation," *SIGPLAN Not.*, vol. 37, no. 1, p. 178190, Jan. 2002. [Online]. Available: https://doi.org/10.1145/565816.503290

[12] E. Miranda, “The cog smalltalk virtual machine,” in *Proceedings of VMIL 2011*, 2011.

[13] D. Ungar, "Generation Scavenging: A Non-Disruptive High Performance Storage Reclamation Algorithm," *ACM SIGPLAN Notices*, vol. 19, no. 5, pp. 157-167, 1984. [Online]. Available: https://doi.org/10.1145/390011.808261

[14] L. Birkedal and M. Welinder, “Hand-writing program generator generators,” in *Proceedings of the 6th International Symposium on Programming Language Implementation and Logic Programming*, ser. PLILP '94. Berlin, Heidelberg: Springer-Verlag, 1994, p. 198214. [Online]. Available: https://doi.org/10.1007/3-540-58402-1_15

[15] R. Cytron, J. Ferrante, B. K. Rosen, M. N. Wegman, and F. K. Zadeck, "Efficiently computing static single assignment form and the control dependence graph," *ACM Trans. Program. Lang. Syst.*, vol. 13, no. 4, pp. 451-490, 1991. [Online]. Available: https://doi.org/10.1145/115372.115320

[16] C. Chambers, "Staged compilation," *SIGPLAN Not.*, vol. 37, no. 3, p. 18, Jan. 2002. [Online]. Available: https://doi.org/10.1145/509799.503045

[17] E. Miranda, C. Béra, E. G. Boix, and D. Ingalls, "Two decades of Smalltalk VM development: live VM development through simulation tools," in *Proceedings of International Workshop on Virtual Machines and Intermediate Languages (VMIL'18)*. ACM, 2018, pp. 57-66. [Online]. Available: https://doi.org/10.1145/3281287.3281295

[18] S. Marr, "Rebench: Execute and document benchmarks reproducibly," aug 2018, version 1.0. [Online]. Available: https://doi.org/10.5281/zenodo.1311762

[19] S. Marr, B. Daloze, and H. Mössenböck, "Cross-language compiler benchmarking: Are we fast yet?" in *Proceedings of the 12th Symposium on Dynamic Languages*, ser. DLS 2016. New York, NY, USA: Association for Computing Machinery, 2016, pp. 120-131. [Online]. Available: https://doi.org/10.1145/2989225.2989232

[20] S. Brunthaler, "Efficient interpretation using quickening," *SIGPLAN Not.*, vol. 45, no. 12, p. 114, Oct. 2010. [Online]. Available: https://doi.org/10.1145/1899661.1869633

[21] —, "Inline caching meets quickening," in *Proceedings of the 24th European Conference on Object-Oriented Programming*, ser. ECOOP'10. Berlin, Heidelberg: Springer-Verlag, 2010, pp. 429-451. [Online]. Available: https://doi.org/10.1007/978-3-642-14107-2_21

[22] C. Béra and E. Miranda, "A bytecode set for adaptive optimizations," in *International Workshop on Smalltalk Technologies (IWST)*, Aug. 2014. [Online]. Available: http://hal.inria.fr/hal-01088801

[23] J. Aycock, "A brief history of just-in-time," *ACM Comput. Surv.*, vol. 35, no. 2, p. 97113, Jun. 2003. [Online]. Available: https://doi.org/10.1145/857076.857077
[24] J. McCarthy, *History of LISP*. New York, NY, USA: Association for Computing Machinery, 1978, p. 173185. [Online]. Available: https://doi.org/10.1145/800025.1198360

[25] L. P. Deutsch and A. M. Schiffman, "Efficient implementation of the Smalltalk-80 system," in *Proceedings POPL '84*, Salt Lake City, Utah, Jan. 1984. [Online]. Available: https://doi.org/10.1145/800017.800542

[26] D. Ungar and R. B. Smith, "Self: The Power of Simplicity," in *Proceedings OOPSLA '87, ACM SIGPLAN Notices*, vol. 22, Dec. 1987, pp. 227-242. [Online]. Available: https://doi.org/10.1145/38765.38828

[27] S. Dissegna, F. Logozzo, and F. Ranzato, "Tracing compilation by abstract interpretation," *SIGPLAN Not.*, vol. 49, no. 1, p. 4759, Jan. 2014. [Online]. Available: https://doi.org/10.1145/2578855.2535866

[28] —, "An abstract interpretation-based model of tracing just-in-time compilation," *ACM Trans. Program. Lang. Syst.*, vol. 38, no. 2, Jan. 2016. [Online]. Available: https://doi.org/10.1145/2853131

[29] T. Kotzmann, C. Wimmer, H. Mössenböck, T. Rodriguez, K. Russell, and D. Cox, "Design of the java hotspot client compiler for java 6," *ACM Trans. Archit. Code Optim.*, vol. 5, no. 1, May 2008. [Online]. Available: https://doi.org/10.1145/1369396.1370017

[30] M. Paleczny, C. Vick, and C. Click, "The java hotspotTM server compiler," in *Symposium on JavaTM Virtual Machine Research (JVM'01)*, ser. JVM'01. USENIX Association, 2001. [Online]. Available: http://www.usenix.org/events/jvm01/full_papers/paleczny/paleczny.pdf

[31] V. Team, "Maglev: A new jit compiler for v8," 2022, accessed: 2025-05-26. [Online]. Available: https://v8.dev/blog/maglev

[32] M. W. Contributors, "Ionmonkey mir," n.d., accessed: 2025-05-26. [Online]. Available: https://wiki.mozilla.org/IonMonkey/MIR

[33] Q. Zhang, L. Xu, and B. Xu, "Python meets jit compilers: A simple implementation and a comparative evaluation," *Software: Practice and Experience*, vol. 54, no. 2, pp. 225-256, 2024. [Online]. Available: https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.3267

[34] M. Bebenita, F. Brandner, M. Fahndrich, F. Logozzo, W. Schulte, N. Tillmann, and H. Venter, "Spur: a trace-based jit compiler for cil," in *Proceedings of the ACM International Conference on Object Oriented Programming Systems Languages and Applications*, ser. OOPSLA '10. New York, NY, USA: Association for Computing Machinery, 2010, p. 708725. [Online]. Available: https://doi.org/10.1145/1869459.1869517

[35] A. Georges, D. Buytaert, and L. Eeckhout, "Statistically rigorous java performance evaluation," in *Proceedings of the 22nd Annual ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages and Applications*, ser. OOPSLA '07. New York, NY, USA: Association for Computing Machinery, 2007, pp. 57-76. [Online]. Available: https://doi.org/10.1145/1297027.1297033

[36] A. Haas, A. Rossberg, D. L. Schuff, B. L. Titzer, M. Holman, D. Gohman, L. Wagner, A. Zakai, and J. Bastien, "Bringing the web up to speed with webassembly," *SIGPLAN Not.*, vol. 52, no. 6, p. 185200, Jun. 2017. [Online]. Available: https://doi.org/10.1145/3140587.3062363

[37] W. H. Li, D. R. White, and J. Singer, "Jvm-hosted languages: they talk the talk, but do they walk the walk?" in *Proceedings of the 2013 International Conference on Principles and Practices of Programming on the Java Platform: Virtual Machines, Languages, and Tools*, ser. PPPJ '13. New York, NY, USA: Association for Computing Machinery, 2013, p. 101112. [Online]. Available: https://doi.org/10.1145/2500828.2500838

[38] A. Sarimbekov, A. Podzimek, L. Bulej, Y. Zheng, N. Ricci, and W. Binder, "Characteristics of dynamic jvm languages," in *Proceedings of the 7th ACM Workshop on Virtual Machines and Intermediate Languages*, ser. VMIL '13. New York, NY, USA: Association for Computing Machinery, 2013, p. 1120. [Online]. Available: https://doi.org/10.1145/2542142.2542144

[39] A. Rigo and S. Pedroni, "PyPy's approach to virtual machine construction," in *Proceedings of the 2006 conference on Dynamic languages symposium*. New York, NY, USA: ACM, 2006, pp. 944-953. [Online]. Available: https://doi.org/10.1145/1176617.1176753

[40] D. Ancona, M. Ancona, A. Cuni, and N. Matsakis, "RPython: a step towards reconciling dynamically and statically typed oo languages," in *Proceedings of the 2007 Symposium on Dynamic Languages (DSL 07)*. ACM, 2007, pp. 53-64. [Online]. Available: https://doi.org/10.1145/1297081.1297091

[41] C. F. Bolz, A. Lienhard, N. D. Matsakis, O. Nierstrasz, L. Renggli, A. Rigo, and T. Verwaest, "Back to the future in one week — implementing a Smalltalk VM in PyPy," in *Self-Sustaining Systems*, ser. LNCS, vol. 5142. Springer, 2008, pp. 123-139. [Online]. Available: http://scg.unibe.ch/archive/papers/Bolz08aSpy.pdf

[42] C. Bruni and T. Verwaest, "PyGirl: Generating whole-system VMs from high-level prototypes using PyPy," in *Objects, Components, Models and Patterns, Proceedings of TOOLS Europe 2009*, ser. LNBIP, vol. 33. Springer-Verlag, 2009, pp. 328-347. [Online]. Available: http://scg.unibe.ch/archive/papers/Brun09cPyGirl.pdf

[43] C. Wimmer and T. Würthinger, "Truffle: a self-optimizing runtime system," in *Proceedings of the 3rd annual conference on Systems, programming, and applications: software for humanity*, 2012, pp. 13-14. [Online]. Available: https://doi.org/10.1145/2384716.2384723

[44] G. Duboscq, L. Stadler, T. Würthinger, D. Simon, C. Wimmer, and H. Mössenböck, "Graal ir: An extensible declarative intermediate representation," in *Asia-Pacific Programming Languages and Compilers Workshop*, 2013. [Online]. Available: https://www.academia.edu/download/76699729/APPLC-2013-paper_12.pdf

[45] F. Niephaus, T. Felgentreff, and R. Hirschfeld, "Graalsqueak: Toward a smalltalk-based tooling platform for polyglot programming," in *Proceedings of the 16th ACM SIGPLAN International Conference on Managed Programming Languages and Runtimes (MPLR'19)*. New York, NY, USA: ACM, 2019, pp. 14-26. [Online]. Available: https://doi.org/10.1145/3357390.3361024

[46] A. Wöss, C. Wirth, D. Bonetta, C. Seaton, and C. Humer, "An Object Storage Model for the Truffle Language Implementation Framework," in *PPJP'14*, 2014. [Online]. Available: https://doi.org/10.1145/2647508.2647517

[47] C. Seaton, M. L. Van De Vanter, and M. Haupt, "Debugging at full speed," in *Proceedings of the Workshop on Dynamic Languages and Applications*, 2014, pp. 1-13. [Online]. Available: https://doi.org/10.1145/2617548.2617550

[48] V. Bala, E. Duesterwald, and S. Banerjia, "Dynamo: A transparent dynamic optimization system," in *Proceedings of the ACM SIGPLAN 2000 Conference on Programming Language Design and Implementation*, ser. PLDI '00. New York, NY, USA: Association for Computing Machinery, 2000, pp. 1-12. [Online]. Available: https://doi.org/10.1145/349299.349303

[49] D. R. Engler, "Vcode: a retargetable, extensible, very fast dynamic code generation system," in *Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language Design and Implementation*, ser. PLDI '96. New York, NY, USA: Association for Computing Machinery, 1996, p. 160170. [Online]. Available: https://doi.org/10.1145/231379.231411

[50] J. Lim and T. Reps, "Tsl: A system for generating abstract interpreters and its application to machine-code analysis," *ACM Trans. Program. Lang. Syst.*, vol. 35, no. 1, Apr. 2013. [Online]. Available: https://doi.org/10.1145/2450136.2450139

[51] D. Boucher and M. Feeley, "Abstract compilation: A new implementation paradigm for static analysis," in *Compiler Construction*, T. Gyimóthy, Ed. Berlin, Heidelberg: Springer Berlin Heidelberg, 1996, pp. 192-207. [Online]. Available: https://doi.org/10.1007/3-540-61053-7_62

[52] P. Cousot, R. Giacobazzi, and F. Ranzato, "Aši: abstractš interpretation," *Proc. ACM Program. Lang.*, vol. 3, no. POPL, Jan. 2019. [Online]. Available: https://doi.org/10.1145/3290355
Our set of classical **micro benchmarks**<fnref n="3" /> is as follows:

* *BinaryTrees*. An adaptation of *Hans Boehm's GCBench* for Garbage Collection.

* *Chameleons*. Creates many threads to wait for mutex semaphores.

* *ChameneosRedux*. Small problem size for *Chameleons* benchmark.

* *DeltaBlue*. Classic object-oriented *constraint solver*.

* *Fasta*. DNA sequence generation algorithm based on weighted random selection.

* *KNucleotide*. Map the DNA letters into a hash table to accumulate count values.

* *Mandelbrot*. Plot the Mandelbrot set on an N-by-N bitmap.

* *Meteor*. Uses *ByteStrings* to solve a puzzle.

* *NBody*. Classic n-body simulation of the solar system.

* *PiDigits*. Generates and prints the first N digits of Pi.

* *RegexDNA*. A simple regex pattern and actions to manipulate FASTA format data.

* *ReverseComplement*. computes the reverse complement of a DNA sequence.

* *Richards*. Simulates a *task dispatcher* on a multitasking operating system.

* *Slopstones*. *Smalltalk Low-level Operation Stones*, a series of low-level operations.

* *SpectralNorm*. Calculate the spectral norm of an N-by-N matrix.

In addition, we run some applications as **macro benchmarks**:

* *Compiler*. Tests for the source-to-bytecode Pharo compiler.

* *File*. Tests for file system access library.

* *Kernel*. Tests for basic language features.

* *Microdown*. Tests for the markup documentation library.

* *Startup*. Just evaluate 1 + 1, measure the time for starting up the system.

<footnote n="3">³Implementations are in https://github.com/guillep/SMark visited on 2024-02-04.</footnote>
APPENDIX B: *Static Type Prediction* FOR ADDITION BYTECODE

In this appendix, we present a full version of the addition bytecode handler, performing *Static Type Prediction* for integers,
for the interpreter and its metacompiled version for the baseline JIT compiler.

The interpreter handler uses *intrinsics* to guide the metacompilation process, as presented in Figure 14. They are used to
ignore the metacompilation of several paths based on metac options. This allows us to generate different versions of
the JIT compiler easily.

Due to *Static Type Prediction* optimization, the metacompiled generator handler has a **fast path**, where type are predicted and the addition is performed, and a **slow path**, performing a normal message send. Figure 15 presents the result of the metacompilation process. The abstract stack is *spilled* for performing the message send (runtime service), and *unspilled* for the case where type are predicted and the result is computed. This version manages the abstract state correctly over different runtime branches following the transformations presented in Section III.

Interpreter >> bytecodeAdd
<compilationInfo: #isMapped>
<druidInfo: #hasSend>

| rcvr arg |
rcvr := self stackValue: 1.
arg := self stackValue: 0.
(objectMemory areIntegers: rcvr and: arg)
    ifTrue: [
        self druidIf: #staticTypePrediction do: [
            self
                sumSmallInteger: rcvr
                withSmallInteger: arg
                ifNotOverflow: [ :result |
                    self pop: 2 thenPush: result.
                    ^ self fetchNextBytecode "success" ] ] ]
    ifFalse: [ "Do not JIT compile float case..."
        self druidIgnore: [
            self initPrimCall.
            self primitiveFloatAdd: rcvr toArg: arg.
            self successful ifTrue: [ ^ self fetchNextBytecode "success" ] ] ].

self normalSendSpecialSelector: 0 argumentCount: 1

<caption>Fig. 14: Bytecode handler for addition ("+" operation) performing *Static Type Prediction* for integers and floats in the interpreter.
The code uses *intrinsics* to guide the metacompilation of the generator handler. For integers, this optimization is metacompiled
based on the #staticTypePrediction option. The case for floats is always ignored using the druidIgnore: intrinsic.</caption>
```
JITCompiler >> gen_BytecodePrimAdd
"AutoGenerated by Druid"

| t0 jump3 jump1 t1 currentBlock jump2 live t2 |
live := 0.
t0 := self allocateRegNotConflictingWith: live ifNone: [ ^ self unknownBytecode ].
live := live bitOr: (self registerMaskFor: t0).
t1 := self allocateRegNotConflictingWith: live ifNone: [ ^ self unknownBytecode ].
live := live bitOr: (self registerMaskFor: t1).
t2 := self allocateRegNotConflictingWith: live ifNone: [ ^ self unknownBytecode ].
live := live bitOr: (self registerMaskFor: t2).

"Load values"
(self ssValue: 1) copyToReg: t0.
(self ssValue: 0) copyToReg: t1.

"Test for both integers"
self MoveR: t0 R: t2.
self AndR: t1 R: t2.
self ssFlushStackExceptTop: 2. "Spill for message send"
self TstCq: 1 R: t2.
jump1 := self JumpZero: 0.

"Perform the integer addition"
self AddCq: -1 R: t0.
self AddR: t1 R: t0.
jump2 := self JumpOverflow: 0.

"No overflow, success"
self ssUnspillStackSlotAt: 0. "Unspill argument"
self ssUnspillStackSlotAt: 1. "Unspill receiver"
self MoveR: t0 R: t2.
jump3 := self Jump: 0.

"Message send"
currentBlock := self Label.
jump1 jmpTarget: currentBlock.
jump2 jmpTarget: currentBlock.
self marshallSendArgumentsNoPush: 1.
self genMarshalledSendNoPush: -1 numArgs: 1 sendTable: ordinarySendTrampolines.
self MoveR: ReceiverResultReg R: t2.

"Pop values and push result"
currentBlock := self Label.
jump3 jmpTarget: currentBlock.
self ssPop: 2 popSpilled: false.
self ssPushRegister: t2.
^ 0
```

<caption>Fig. 15: Metacompiled generator handler from previous Figure 14 performing *Static Type Prediction* for integer values. This version manages the abstract state correctly over different runtime path following the transformations presented in Section III.</caption>