Skip to content

Commit 341daf3

Browse files
committed
Add blog post about PGO
1 parent b384b7e commit 341daf3

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

_posts/2026-05-25-native-pgo.adoc

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
layout: post
3+
title: 'Profile-Guided Optimization for Quarkus Native Images'
4+
date: 2026-05-27
5+
tags: performance native graalvm pgo
6+
synopsis: 'Quarkus 3.35 introduces Profile-Guided Optimization (PGO) support for native images. Enable it with quarkus.native.pgo.enabled=true and let your integration tests drive the profiling - the build automatically produces an optimized native binary tailored to your application''s actual runtime behavior.'
7+
author: geoand
8+
---
9+
10+
Native images are fast. But what if they could be even faster?
11+
12+
Profile-Guided Optimization (PGO) is a compiler technique that has been used in native code compilation for decades. The idea is simple: run your application with a representative workload, collect profiling data about which code paths are hot, then recompile with that knowledge to produce a better-optimized binary.
13+
14+
GraalVM has supported PGO for years, but using it required manual steps: build an instrumented binary, run it with your workload, collect the profile, then rebuild with the profile. This workflow didn't fit naturally into the typical Quarkus development cycle.
15+
16+
Starting with Quarkus 3.35, we've integrated PGO directly into the native build process. The same integration tests you already write to verify your application now automatically drive the profiling. One flag, one build command, and you get a PGO-optimized native image.
17+
18+
== What is Profile-Guided Optimization?
19+
20+
Profile-Guided Optimization is a two-phase compilation technique:
21+
22+
Phase 1 - Instrumentation::
23+
The compiler generates an instrumented binary that records execution data as it runs. This binary is slightly slower and larger than a normal build because it contains profiling instrumentation.
24+
25+
Phase 2 - Optimization::
26+
The compiler uses the collected profile data to make better optimization decisions: which methods to inline, how to lay out code for better cache locality, which branches are likely vs. unlikely, and where to focus optimization effort.
27+
28+
The result is a binary that is optimized for your application's actual runtime behavior — which methods are hot, which branches are taken, which types appear at call sites — rather than generic heuristics. The profile is stored in a `.iprof` file that GraalVM's `native-image` tool consumes during the optimized build.
29+
30+
== Getting started
31+
32+
If you already have `@QuarkusIntegrationTest` tests in your project, enabling PGO is a single property:
33+
34+
[source, properties]
35+
----
36+
quarkus.native.pgo.enabled=true
37+
----
38+
39+
Then build as usual (PGO requires a native build, so `-Dnative` is needed):
40+
41+
[source, bash]
42+
----
43+
./mvnw verify -Dnative -Dquarkus.native.pgo.enabled=true
44+
----
45+
46+
The build process now has three phases:
47+
48+
1. **Instrumented build**: Quarkus builds a native image with `--pgo-instrument`. This binary contains profiling instrumentation.
49+
2. **Training run**: Your `@QuarkusIntegrationTest` tests run against the instrumented binary. As they exercise your endpoints and features, the binary writes profiling data to `default.iprof`.
50+
3. **Optimized build**: Quarkus automatically rebuilds the native image with `--pgo=default.iprof`, producing an optimized binary that replaces the instrumented one.
51+
52+
The final binary in `target/` is the PGO-optimized version. The instrumented binary is kept alongside it with an `.instrumented` suffix for reference.
53+
54+
=== Requirements
55+
56+
PGO requires **Oracle GraalVM**. Community builds of GraalVM (including Mandrel and Liberica NIK) do not include PGO support.
57+
58+
If you try to enable PGO with a non-Oracle GraalVM distribution, the build will fail with a clear error message:
59+
60+
[source]
61+
----
62+
Profile-Guided Optimization (PGO) requires Oracle GraalVM.
63+
Detected distribution: MANDREL.
64+
Please use Oracle GraalVM or disable PGO with quarkus.native.pgo.enabled=false
65+
----
66+
67+
You can download Oracle GraalVM from https://www.oracle.com/java/technologies/downloads/#graalvmjava25[oracle.com] or use https://sdkman.io/[SDKMAN]:
68+
69+
[source, bash]
70+
----
71+
sdk install java 25-graal
72+
sdk use java 25-graal
73+
----
74+
75+
=== What makes a good training workload?
76+
77+
The quality of PGO optimization depends entirely on the profiling data. Your `@QuarkusIntegrationTest` tests should exercise the code paths that matter in production:
78+
79+
* **Cover your hot paths**: If 90% of production requests hit a specific endpoint, make sure your tests exercise it heavily.
80+
* **Use realistic data**: If your application processes JSON payloads, use realistic payload sizes and structures in your tests.
81+
* **Include error paths**: If your application handles validation errors or retries, include tests that trigger those paths.
82+
* **Avoid cold paths**: Don't spend test time on rarely-used admin endpoints or debug features unless they're performance-critical.
83+
84+
The instrumented binary is slower than a normal native image (typically 2-3x), so keep your test suite focused. You don't need exhaustive coverage - you need representative coverage of production behavior. You can also use smaller data sizes than production for profiling; the compiler mostly cares about which code paths are hot, not the volume of data flowing through them.
85+
86+
Any `quarkus.native.additional-build-args` you configure apply to both the instrumented and optimized native-image builds, so custom resource configs, serialization configs, or other `native-image` flags carry over automatically.
87+
88+
== How it works internally
89+
90+
The implementation follows the same pattern we used for Project Leyden AOT support. When `quarkus.native.pgo.enabled=true` is set:
91+
92+
1. **Instrumented build**: The build adds `--pgo-instrument` to the `native-image` arguments and saves the full argument list for the rebuild phase.
93+
94+
2. **Training run**: The test framework adds `-XX:ProfilesDumpFile=default.iprof` when launching the instrumented binary, telling it where to write the profile.
95+
96+
3. **Post-test rebuild**: After integration tests complete, the `build-enhanced-artifact` Maven goal detects `default.iprof` and triggers a second `native-image` build with `--pgo=default.iprof` instead of `--pgo-instrument`.
97+
98+
The rebuild uses the exact same arguments as the original build, ensuring consistency. This means PGO works with all native image configurations — custom resource configs, reflection configs, additional build args, etc. The PGO layer is orthogonal to everything else.
99+
100+
== Performance impact
101+
102+
The actual improvement depends on your application and the quality of the training workload. Applications with hot loops, polymorphic call sites, or complex code paths tend to benefit most. The GraalVM team has https://www.graalvm.org/latest/reference-manual/native-image/guides/optimize-native-executable-with-pgo/[documented significant improvements] in their own benchmarks.
103+
104+
The instrumented binary is larger and slower than a normal native image, but this is only used during the training run. The final optimized binary is the same size as a regular native image.
105+
106+
PGO is complementary to other native image optimizations — you can combine it with G1 GC (`-H:+UseG1GC`), custom resource configs, or any other native-image flags. The build time cost is roughly double (since the native image is built twice), but this is a one-time cost. The optimized binary runs faster for its entire lifetime.
107+
108+
== Integration with container images
109+
110+
PGO works with Quarkus container image extensions, but requires a two-step process today:
111+
112+
[source, bash]
113+
----
114+
# Step 1: Build PGO-optimized native image
115+
./mvnw verify -Dnative -Dquarkus.native.pgo.enabled=true
116+
117+
# Step 2: Build container image with the optimized binary
118+
./mvnw package -Dnative -Dquarkus.container-image.build=true -DskipTests
119+
----
120+
121+
The first command produces the PGO-optimized binary in `target/`. The second command packages it into a container image.
122+
123+
We plan to streamline this into a single command in a future release, similar to what we did for Leyden AOT container images. The goal is:
124+
125+
[source, bash]
126+
----
127+
./mvnw verify -Dnative -Dquarkus.native.pgo.enabled=true -Dquarkus.container-image.build=true
128+
----
129+
130+
This would build the instrumented image, run tests in a container, collect the profile, rebuild with PGO, and produce the final container image - all in one command.
131+
132+
== Differences from Project Leyden
133+
134+
While both PGO and Leyden use profiling to improve performance, they solve different problems:
135+
136+
* **PGO** targets native images. It improves throughput by generating more optimized machine code — better inlining decisions, improved code layout, and more aggressive devirtualization based on observed runtime behavior.
137+
* **Project Leyden** targets the JVM. It aims (among other things) to allow the JVM to reach maximum throughput faster by caching class loading, linking, and JIT compilation work from a training run so subsequent startups skip that warmup cost.
138+
139+
Both techniques use your integration tests as the training workload, and both are enabled with a single flag.
140+
141+
If you're deploying native images, use PGO. If you're deploying on the JVM, use Leyden.
142+
143+
== Troubleshooting
144+
145+
=== Profile not generated
146+
147+
The profile data is written when the instrumented binary shuts down gracefully. If the optimized build doesn't happen, check that:
148+
149+
1. `default.iprof` exists in `target/` after tests run
150+
2. Your tests actually start the application (check test logs)
151+
3. The instrumented binary shut down cleanly (a crash or `SIGKILL` prevents the profile from being written)
152+
153+
Enable verbose logging to see what's happening:
154+
155+
[source, properties]
156+
----
157+
quarkus.log.category."io.quarkus.deployment.pkg".level=DEBUG
158+
----
159+
160+
=== Build fails with "PGO requires Oracle GraalVM"
161+
162+
You're using a GraalVM distribution that doesn't support PGO. Download Oracle GraalVM from https://www.oracle.com/java/technologies/downloads/#graalvmjava25[oracle.com] or use SDKMAN:
163+
164+
[source, bash]
165+
----
166+
sdk install java 25-graal
167+
sdk use java 25-graal
168+
----
169+
170+
=== Optimized binary is slower
171+
172+
This usually means the training workload doesn't match production behavior. Review your `@QuarkusIntegrationTest` tests:
173+
174+
* Do they exercise the same endpoints as production?
175+
* Do they use realistic data sizes and patterns?
176+
* Do they run long enough to collect meaningful profiles?
177+
178+
The instrumented binary needs at least a few seconds of runtime to collect useful data. If your tests complete in milliseconds, the profile will be sparse.
179+
180+
== Conclusion
181+
182+
Profile-Guided Optimization brings a proven compiler technique to Quarkus native images. The integration is designed to be invisible: enable one flag, and your existing integration tests drive the profiling automatically. The cost is a longer build time, but for production deployments where performance matters, that's a trade-off worth making.
183+
184+
We'd like to thank the GraalVM team at Oracle for their collaboration on PGO support. We'll continue tracking GraalVM's PGO development and improving the integration as new capabilities become available.
185+
186+
== Come Join Us
187+
188+
We value your feedback a lot so please report bugs, ask for improvements... Let's build something great together!
189+
190+
If you are a Quarkus user or just curious, don't be shy and join our welcoming community:
191+
192+
* provide feedback on https://github.com/quarkusio/quarkus/issues[GitHub];
193+
* craft some code and https://github.com/quarkusio/quarkus/pulls[push a PR];
194+
* discuss with us on https://quarkusio.zulipchat.com/[Zulip] and on the https://groups.google.com/d/forum/quarkus-dev[mailing list];
195+
* ask your questions on https://stackoverflow.com/questions/tagged/quarkus[Stack Overflow].

0 commit comments

Comments
 (0)