Pitfalls of Benchmarking on Modern Systems

Modern computer systems are quite fascinating. They are highly complex, run a lot of software, and their performance characteristics are hard or impossible to predict.

Earlier this year, I gave a lecture on benchmarking, which for lack of generic and always-applicable advice, I started with a brainstorming session about its pitfalls. Since systems have become so complex that we rarely know what happens exactly. We are prone to simply guess what’s going on. Or rather, we should hypothesize and verify our hypothesis. Still, there might be many more causes for what we see than we can initially think of.

To illustrate this, I’ll use some fictitious benchmark results, which are fairly close to what one would see in reality, and there are usually many possible explanations for the observed behavior. This often means, only once we know what caused a specific artifact, we can make progress with understanding what we set out to understand in the first place.

The Scenario: A “Reasonably Deterministic” Workload

Let’s start with our hypothetical/fictitious benchmark. One rather strong assumption we are going to make is that our benchmark is “reasonably deterministic”. Thus, when we run it multiple times, it pretty much does the same thing. To make this more concrete, let’s assume our benchmark does what one of the very first business applications did: payroll. We have a program that generates PDFs with the monthly payslips. We run our benchmark on a Linux from 2026 and since our hypothetical benchmark is written in Java, we use the HotSpot JVM and JDK 26.

A First Run

We let this benchmark run for 8 iterations within the same JVM process and visualize all our data points with a scatter plot. On the y-axis we show run time, which means lower is better. On the x-axis, we have the number of the specific iteration.

Figure 1: A first run of our benchmark. The x-axis shows the iterations, and the y-axis the run time. Thus, lower is better. We see that each subsequent iteration got faster until iteration 4, and then we stayed at the same performance level.

On the plot we can see that iterations 2, 3, 4 are each a bit faster than the previous one, and then we stay at the same level of performance.

Given that we use a language implementation with just-in-time (JIT) compilation, this is what we would roughly expect. The JIT compiler manages to optimize the code we are executing step by step, and we see that it improves performance.

A Second Run, Same Benchmark, Nothing Changed

We ran the exact same setup a second time, getting 8 new data points. But as we can see in Figure 2, the benchmark is faster in iteration 3 and following! What happened here?

Figure 2: A second run of the same benchmark, with the same setup. Surprisingly, it is quite a bit faster than on the first run!

This is where we now start hypothesizing.

What could it be? It could be that the operating system decided to give it different physical memory, or run it on a different kind of core (many CPUs these days have 2 or more different types of cores with different performance). Perhaps, we ran out of thermal budget and the CPU ran at a lower clock speed to avoid overheating? Or, since we are on a JVM, perhaps the compiler saw slightly different information for the types/behavior seen in the first and second iterations, and thus, made slightly different optimization decisions? This is possible because compilation happens on a background thread, and thus, even when the benchmark is deterministic, the used profiling information is to some degree racy.

It could also be something entirely different however, which means, we do not really know.

For designing your benchmark methodology, you’ll need to decide on how to take these variables into account. Sometimes, you may simply collect data from more runs, ideally many runs, so that you can characterize this behavior as part of the performance distribution one might likely observe in practice. In other cases, this might be too naive, and you need to carefully control for specific variables to get useful data from your experiments. This can include pinning threads to specific cores, fixing CPU frequencies, disabling address space layout randomization, etc. Each approach comes with different tradeoffs.

A Third Run, And More Data

We ran the same benchmark, with the very same setup, a third time. And, we actually had a few more than the 8 data points I showed before.

Figure 3: A third run of the same benchmark, with the same setup, and we had a few more iterations than shown before.

The third run was slower. What happened now? Still guessing, we could assume we simply didn’t get as lucky as the second time. Indeed, we might have gotten rather unlucky, at least until iteration 14. So, the same mechanisms that made the second run faster, could have now played against us, and made the third run slower. And that’s why performance is a distribution.

But, at iteration 15, something else happened in addition. This isn’t a change between runs, i.e., a change between different operating system processes. It’s a change within the same process, and after performance already looked stable.

Indeed, this might be something that happened hours, not minutes, after we thought that performance is stable. We could still be guessing that it is very much the same mechanisms as before though. The operating system may just have decided our workload needs to be handled somehow differently: different core, different core type, different physical memory, etc.

Or it could have been a mechanism somewhere else in the software stack that changed based on some heuristic that triggered only after that time. And indeed, there are various mechanisms hiding. Colleagues saw behavior like this in the JVM, where it would free classes generated internally by the JVM to speed up reflection. The JVM sets a timeout based on the maximum heap size. Using 1 second per MB of heap, which can mean it happens very late in an experiment. Go, read their Experimental Evaluation Methodology for the Era of No Steady Performance paper, it’s a fun read.

Same Benchmark, Different Input

Let’s look at one more set of fictitious results.

Figure 4: Same benchmark, but different input. And, there has been some time between the two runs.

This time, we see a more irregular pattern. Though, we still see a major performance difference between the two runs.

Our hypotheses might include all of the above, but perhaps additionally, it could include garbage collection (GC) more generally. We see a bit of an up and down. So, perhaps the GCs are not fully part of every iteration, as we might guess compared to the previous plots. Thus, here we might see the impact of the JVM’s garbage collector.

But, it could also be something about the dates at which the experiments ran.

Did we run this on a laptop? Was it plugged into power in the past, and now isn’t, or the other way around? Was it cold in February, and now it’s hot, so, the CPU clocks itself down? Did we update the software between these runs?

It really could be a million different things.

All we know is that it is probably not safe to assume that the data is comparable. And that’s really the one thing we always need to investigate: Are we confident that the data we obtained is comparable, or has some hidden variable changed that we did not account for?

Pitfalls When Benchmarking

When benchmarking, there are unfortunately a lot of things we may need to consider. Some of them, we can find ways to account, i.e., control for. Though, that’s at the risk of reviewers saying that the setup is unrealistic. (I’d argue, explainability trumps realism in many cases.) Other pitfalls, we might only be able to address by collecting more data. And yet others might mean we need to start all over again.

Since this is a rather complex area, I do not have any quick and simple solutions. So, let’s conclude with my incomplete overview of things to keep in mind:

  • Run-time optimizations
    • Feedback-based compiler decisions
    • Memory changes, garbage collection
  • Security mechanisms
    • Address space layout randomization
  • Hardware complexity
    • Memory hierarchies, caches, locality
    • Cores, core types, …
    • Dynamic clock frequency changes
      • Thermal budgets
  • Environmental impact
    • Temperature
    • Power source
  • Software updates/changes
    • Linking order

And many more…

SSW@ECOOP'26: On Debugging, Benchmarking and (Meta-)Compilation

At this year’s ECOOP, the Institute for System Software will be attending with the almost complete team and we’re going to present on a variety of topics. Come and talk to us at ICOOOLPS, MPLR, the Demo Track, DEBT, ECOOP Academy, and at the poster session!

Below, a brief overview and when the talks are scheduled.

Monday, June 29, 2pm: AOT Meta-Compilation of Dynamic Languages

Towards Ahead-of-Time Meta-Compilation of Dynamic Languages With an Extensible Type Analysis

Christoph A. is going to present initial ideas of how to approach ahead-of-time meta-compilation for dynamic languages. While some dynamic languages can already be compiled fairly successfully ahead of time, we would love to get the compiler for free, ideally from not much more than having to implement the interpreter.

Preprint of the ICOOOLPS position paper.

Full Abstract

Dynamically-typed languages rely on just-in-time (JIT) compilation for execution performance. Meta-compilation systems such as GraalVM's Truffle language implementation framework have reduced the effort needed of enabling JIT compilation to implementing an interpreter. But dynamic languages are increasingly used in scenarios where ahead-of-time (AOT) compilation would be preferable, for instance, for faster startup or to avoid the memory cost of JIT compilation. Therefore, we plan to extend meta-compilation systems to also support AOT compilation.

For successful AOT compilation of dynamically-typed languages, we need an extensive and robust type analysis. In this position paper, we present first ideas for a framework with an extensible core analysis that will enable us to extract type flow semantics from an interpreter implemented in a meta-compilation system.

To achieve the precision needed for fast machine code, we will need to include heuristic analyses. For this, we envision a plugin system that allows us to integrate various different heuristics into a singular unified analysis. Combining analyses in this way can produce results that are better than the sum of their parts.

While this is a very ambitious goal, given the complexity of compiling dynamic languages, we believe we can achieve better-than-interpreted performance for programs with reasonable behavior. Furthermore, to support the full language semantics we keep a general interpreter as a fallback.

Monday, June 29, 3pm: Pedagogical Annotations in a Debugger

Towards Guided Omniscient Debugging in Education using Pedagogical Execution Traces

Markus is going to present work on teaching programming by using debugging techniques. Specifically, he will look at enriching program visualizations with explanations and interactive questions.

Preprint of the DEBT paper.

Full Abstract

Educators frequently use trace-based debuggers for live classroom demonstrations. Yet, if a student’s attention drops during class, they have to fall back to watching recordings (providing a passive, non-interactive experience) or replaying the debugging session at home (lacking the instructor’s pedagogical context and verbal explanations). We introduce Pedagogical Execution Traces (PETs), a concept that enriches execution traces with explanations, highlights and interactive questions. In this work-in-progress idea paper, we present the conceptual foundation of PETs as interactive learning artifacts, showing their applicability within JavaWiz, an educational trace-based graphical debugger. We explore PET authoring design goals and outline ongoing work regarding collaborative debugging scenarios and leveraging Large Language Models (LLMs) for trace annotation.

Tuesday, June 30, 4pm: Supporting Different GCs in AOT-compiled Binaries

A Unifying Approach to Supporting Multiple Garbage Collectors in AOT-compiled Binaries

Thomas will present a fairly simple but effective approach to enable a single AOT-compiled binary for a Java program to use different GCs. At the moment it supports HotSpot's G1 and a more basic generational GC.

Preprint of the MPLR paper.

Full Abstract

Some language implementations combine garbage collection with ahead-of-time compilation to produce self-contained executables for managed-language programs. In these systems, one can typically choose a garbage collector (GC) only at build time. To use another GC, e.g., for better performance,one needs to build another executable.

In this paper, we present an approach for supporting multiple GCs in the same self-contained executable using unified barriers, object layout, object header, and dynamic dispatch. This enables developers to select a GC at run time. Additionally, isolates, i.e., lightweight virtual machine instances with separate collected heaps but within the same process, can now use different GCs alongside each other.

We evaluate our approach in GraalVM Native Image, supporting the Garbage First (G1) and the Serial GC in the same executable. Our evaluation on the DaCapo Chopin and Renaissance benchmarks shows that G1 has on average no performance change (min. −9 %, max. 14 %). Serial GC shows a peak performance regression of 11 % (min. −10 %, max. 33 %). We believe the simplicity of the approach and that one can now choose the GC at run time and on a per isolate basis make this overhead acceptable.

Tuesday, June 30, 5pm: Reducing Binary Size with Static Heuristics

To Compile or Not To Compile: Evaluating Static Heuristics to Reduce Binary Size of Hybrid Execution Systems

Christoph P. will present his evaluation of how far one can get with basic static compiler heuristics, when it comes to reducing the size of AOT-compiled Java binaries, while minimizing the impact on performance.

Preprint of the MPLR paper.

Full Abstract

To compile, or not to compile, that is the question: When ’tis nobler to optimize for performance. Modern compilers have many different optimizations and optimization goals. A common one is to balance between peak performance and startup time. A new ahead-of-time compiled native executable that embeds a managed runtime tries to offer both, while solidifying the notion that everything should be compiled. However, the cost of an enlarged binary size raises the question whether it is beneficial to compile everything.

In this paper, we evaluate static heuristics from classical AOT compilers as well as other techniques based on our own observations. Our goal is to identify heuristics that work in a compilation-first environment and that allow us to reduce binary size while maintaining peak performance.

We compare the different policies in a closed-world hybrid execution system for Java, based on GraalVM Native Image, on a set of 5 DaCapo and 13 Renaissance benchmarks. We find that with the best combination of heuristics we can reduce binary size by 20% while slowing down average performance by only 4%, but avoiding the need for any run-time feedback or complex machine-learning-based approaches. The most promising combination for production use combines heuristics based on early returns, estimated CPU cycles, number of parameters, and whether a method is a static initializer.

Date TBC: A Debugger for Teaching Threads and Locks

JavaWiz ThreadViz - A Visual Debugger for Multi-threaded Programs Based on the Espresso Java VM

Melissa is going to present a visual debugger designed for teaching threads and locks in Java. Threads, locks, and their interaction can feel hard to explain, though, with the right representation in a debugging tool, their dynamic interactions can become more understandable.

Preprint of the Demo paper.

Full Abstract

Programming novices often face difficulties understanding how multi-threading works. Visual debuggers such as JavaWiz can support beginners by providing dynamic visualizations of a program’s behavior, however, they usually only work for single-threaded programs. This paper presents ThreadViz, an extension of JavaWiz to support visualizing multi-threaded Java programs.

In ThreadViz, thread information is collected for visualization by using the Truffle Debug API. Instead of real concurrency, threads are executed stepwise, allowing the user to determine the order of execution and preventing any unpredictable behavior. In the user interface, a unique color is associated with each thread to illustrate the effects of different synchronization mechanisms such as locking and indicate thread state changes. To conclude, application examples are presented to highlight the tool's capabilities.

Friday, July 3, 11am: A Lecture on Benchmarking

Benchmarking on Modern Hardware: Techniques for Performance Comparisons from Day-To-Day Experimenting to Paper Writing

Last but not least, I'll give a lecture on benchmarking. Modern hard- and software makes that quite a bit more complicated than what we would like it to be and I will show a bit how we approach it in practice.

Full Abstract

Modern systems are great! In many ways, they adapt to our software, and optimize it, despite us not really knowing what we are doing, and to a degree that would have been considered magic just a few decades ago.

Though, once we develop our own research ideas on top of these systems and want to make any argument about performance, all this “magic” makes it hard to understand what measurements mean. Worse yet, making sensible performance claims means we have to understand a good chunk of it. Is this benchmark 20% faster because of what I did, or did the CPU increase the clock frequency for the new but not for the old code? Did the JVM just trigger garbage collection? Did the just-in-time compiler slow down my code? What do you mean, “efficiency core”?

In this lecture, we will have a brief look at why benchmarking on modern systems is hard and what can go wrong. Then we will discuss a range of different research scenarios to get a better feeling of what we may need for our work. Since much of this work may involve gradually building up our own systems, we will also look at what it takes to build them based on reliable feedback.

In the second part, we will look at how we can turn the often chaotic scientific process, with all its trials and errors, into a “scientific engineering process” that enables us to try and try again. I’ll suggest a process that allows us to use the same setup that we use for developing our system to not just understand its performance, but also use it to run the experiments we may want for a scientific paper. I’ll demonstrate how to go from daily pull requests with continuous performance tracking to generating plots and statistics for direct inclusion in LaTeX.

Programming Language Implementation: In Theory, We Understand. In Practice, We Wish We Would.

It’s February! This means I have been at the JKU for four months. Four months with teaching Compiler Construction and System Software, lots of new responsibilities (most notably signing off on telephone bills and coffee orders…), many new colleagues, and new things to learn for me, not least because of the very motivated students and PhD students here. And when I say motivated, yes, I am very surprised. While the attendance of my 8:30am Compiler Construction lectures was declining throughout the term as expected, the students absolutely aced their exam. I suspect I will have to make it harder next year. Much harder… hmmm 🤔 Much of the good results can likely be attributed to the very extensive exercise sessions run by my colleagues throughout the semester.

At this point, I have to send a big thank you to everyone from the Institute for System Software, past and present. It’s great to be part of such a team! You made my start very easy, and, well, it now gives me the time to think about my inaugural lecture.

What’s an inaugural lecture?

I have been in academia for almost two decades, but I have to admit, I don’t really remember being at an inaugural lecture. According to Wikipedia, in the Germanic tradition an inaugural lecture (Antrittsvorlesung) is these days something of a celebration. It’s a festive occasion for a new professor to present their field to a wider audience, possibly also presenting their research vision.

At the JKU, it indeed seems to be planned as a festive occasion, too.

On March 9th, 2026, starting at 4pm Prof. Bernhard Aichernig and I will give our Antrittsvorlesungen, and you are cordially invited to attend.

Bernhard will give a talk titled Verification, Falsification, and Learning – a Triptych of Formal Methods for Trustworthy IT Systems.

My own talk is titled, as is this post: Programming Language Implementation: In Theory, We Understand. In Practice, We Wish We Would.

Bernhard will start out by looking at the formal side of things, making the connection between proving correctness, testing systems in the context of where they are used, and learning models from observable data. My talk will narrow in on language implementations, but also look at how formal correctness is helping us there. Unfortunately, provably-correct systems still elude us for many practical languages. Even worse, we are at a point where we rarely understand what’s going on in enough detail to improve performance or perhaps fix certain rare bugs.

If you like to attend, please register here.

In Theory, We Understand. In Practice, We Wish We Would

Here’s the abstract of my talk:

Our world runs on software, but we understand it less and less. In practice, the complexity of modern systems drains your phone’s battery faster, increases the cost of hosting applications, and consumes unnecessary resources, for instance, in AI systems. All because we do not truly understand our systems any longer. Still, at a basic level, we can fully understand how computers work, from transistors to processors, machine language, all the way up to high-level programming languages.

The convenience of contemporary programming languages is however bought with complexity. Over the last two decades, I admit, I added to that complexity. In the next two decades, I hope we can learn to build programming languages in ways that we can prove to be correct, enable us to generate their implementations automatically, and let systems select optimizations in a way that we can still understand the implications for software running on top of it.

You may now wonder where to go from here. And that’s a very good question. I have another month to figure that out, perhaps more… 😅

So, maybe see you in March?

Until then, suggestions, questions, and complaints, as usual on Mastodon, BlueSky, and Twitter.

Older Posts

Subscribe via RSS