Sitemap
graalvm

GraalVM team blog - https://www.graalvm.org

SkipFlow: Producing Smaller Executables with GraalVM

7 min readApr 16, 2025

--

Press enter or click to view image in full size

Points-to analysis is a crucial part of every GraalVM Native Image build. In this blog post, we present SkipFlow, an extension to the points-to analysis that tracks primitive values and evaluates branching conditions during the analysis process. In our benchmarks, this reduced binary size by an average of 6.35% without increasing build time.

We start by providing a high-level overview of the static analysis in GraalVM Native Image. Then, we describe how SkipFlow improves the points-to analysis. Finally, we present an experimental evaluation and discuss future research in this area.

Points-to Analysis in GraalVM Native Image

A typical Java application has a lot of third-party libraries but uses only a fraction of their functionality. To avoid compiling methods that are not needed, Native Image performs a whole-program points-to analysis to determine all classes, methods, and fields that are so-called reachable, i.e., might be needed at runtime. Consider the example below to understand the motivation for using points-to analysis in this context.

void f(Iface i){
if(i == null) throw new NullPointerException();
if(i instanceof A){ ... }
i.run();
}

By inspecting the method f only, we have little knowledge about the possible value of the parameter i. Therefore, we would have to assume that i.run() can call any implementation of the run() method of any subtype of Iface, which would lead to marking all such methods as reachable.

On the other hand, if we could compute that i will always be of a specific type B (not a subtype of A) and never null, we could replace the virtual invocation of i.run()with a direct invocation of the run() method available on the type B, therefore marking only a single method as reachable. Furthermore, we could remove both the null and type checks (including any code inside the corresponding branches). Points-to analysis tracks the possible types for all variables and fields in the program, making such optimizations possible. Employing points-to analysis leads to both smaller binaries and faster compile times.

Native Image runs the analysis using a data structure called a type flow graph. As the name suggests, this graph models the flow of types throughout the program. Nodes in the graph, which we call flows, represent method parameters, fields, variables, and various instructions relevant to the analysis. Directed use edges describe how the types can flow between nodes. Each flow maintains a typestate representing a set of types that can reach the given flow.

The type flow graph is expanded during the analysis. It starts from a set of root methods (e.g., main()) and gradually adds more nodes and edges as additional reachable code elements are discovered. The analysis enables pruning unreachable code, eliminating redundant type and null checks, and even devirtualizing method invocations for which only a single target method is computed. In the example above, we could reduce the content of the method f to a direct call to B.run().

However, precision is not the only metric we consider. Since the analysis is executed during every Native Image build, analysis time and memory footprint are equally important. We need an analysis that is both reasonably precise and fast. In some cases, we even sacrifice a bit of precision to improve scalability. For example, we use a technique called saturation, which works as follows: if the number of types observed for a given variable exceeds a given threshold (by default 20), the analysis stops tracking the flow of types precisely for that variable and removes it from the graph. This technique is motivated by compilers typically optimising only cases with few possible types. Saturation enables linear scaling with respect to the size of the program, yet with only a negligible effect on precision. To learn more about saturation, you can take a look at this publication: Scaling Type-Based Points-to Analysis with Saturation.

Introducing SkipFlow

The analysis Native Image currently runs (which we will denote as baseline in the rest of the blog) can be needlessly imprecise on specific code patterns. Consider the example below taken from the Dacapo Sunflow benchmark.

void render(..., Display display){
if (display == null) {
display = new FrameDisplay();
}
...
}

The baseline analysis is able to determine that the parameter display will never be null. However, since it does not consider the control flow of the method, it still analyses the contents of the branch and considers the type FrameDisplay as instantiated. This might not seem to be a big problem, but the value of the variable display is eventually used as a receiver to call the imageBegin() method and FrameDisplay.imageBegin() transitively calls into AWT and Swing GUI libraries, none of which are actually needed. This means we analyse and subsequently compile a lot of dead code. Can we do better?

There are many techniques in the area of static analysis which could improve the precision of the baseline analysis, but they are typically too slow for our use case. Remember that the analysis runs in every build and has to process hundreds of thousands of methods in minutes or even faster.

The approach we took for increasing the precision of the analysis, called SkipFlow, is based on two key features. The first feature is evaluating branching conditions during the run of the analysis. To support this feature in the existing framework, we have introduced predicate edges into type flow graphs that encode a relationship between a branching condition and nodes within the branches. In the Dacapo Sunflow example above, a predicate edge would lead from the node representing the null check of display to the node representing the FrameDisplay instantiation. All flows with an incoming predicate edge are disabled by default. Such flows can accept values flowing into them via use edges, but they will not push any values further down the graph until enabled by their predicate. This makes it possible to delay evaluating the content of branches, such as the one above that instantiates a FrameDisplay object, until it is determined that the branch may be executed at runtime. In the particular case of Dacapo Sunflow, we were able to reduce the size of the image by more than 50%.

The second feature is tracking primitive values, which proves useful in cases where the condition is factored into a separate method, such as the example below.

void onExit(Thread thread){
if (thread.isVirtual()){
virtualThreads.remove(thread);
}
}


public boolean isVirtual() {
return this instanceof BaseVirtualThread;
}

The SharedThreadContainer.onExit() method, taken from the JDK, is a callback containing code that should be executed only for virtual threads. The check itself is offloaded into the Thread.isVirtual() method, which contains a simple type check. This is a common code pattern in the JDK. Tracking primitive values enables the propagation of boolean values (true and false) out of method calls, so that code specific to virtual threads can be removed unless the compiled application actually uses them.

Experimental Evaluation

We have evaluated SkipFlow using the Renaissance and Dacapo benchmark suites, and a set of microservices applications. Below, we present a chart showing the impact on binary size for a set of microservices applications. SkipFlow reduces the size of native images by 4.4% on average without negatively impacting the build time. In fact, image builds tend to be even slightly faster with SkipFlow enabled because there are fewer methods to analyse and compile.

Press enter or click to view image in full size
Binary Size Reduction for Microservices

Similar trends can be seen in Renaissance and Dacapo benchmark suites, which you can see in the charts below.

Press enter or click to view image in full size
Binary Size Reduction for Dacapo
Press enter or click to view image in full size
Binary Size Reduction for Renaissance

Across all three benchmark suites, binary size is reduced by 6.35%, on average. The evaluation was conducted using GraalVM for JDK 24 on a dual-socket Intel Xeon E5–2630 v3 processor running at 2.40 GHz, with 8 physical / 16 logical cores per socket and 128 GB of main memory, on Oracle Linux Server release 7.3.

Conclusion

SkipFlow is included in GraalVM for JDK 24, but is not yet enabled by default. To enable it, you can use the flags -H:+TrackPrimitiveValues -H:+UsePredicates. However, SkipFlow will be enabled by default in GraalVM for JDK 25 and is already available in Early Access builds! We encourage you to try it on your projects and share your feedback.

In future, we plan to improve SkipFlow analysis even further by using a more precise representation for primitive values, including the possible interval of values that a given variable can have.

If you are interested to learn more, you can take a look at this academic paper SkipFlow: Improving the Precision of Points-to Analysis using Primitive Values and Predicate Edges, which was presented at CGO’25.

--

--

David Kozak
David Kozak

Written by David Kozak

PhD student at the Faculty of Information Technology Brno University of Technology and a Researcher at Oracle Labs. Static Analysis | Compilers | VMs

No responses yet