Repository navigation
CrucibleVM 0.4.0
Pre-releaseCrucibleVM 0.4.0: open-source profile-guided optimization for GraalVM Community Edition native-image, on GraalVM CE 25.4.4.1.1 (JDK 25), Linux x86-64. The fourth downloadable build; 0.3.0 was on 2026-10-26.
Alpha. Measured, and checked for identical output, on a handful of benchmarks and two services; the builder's own unit tests now pass with a profile and in a recording image, but this is not yet the breadth of code a production compiler has to get right. Options and the profile format may change. Keep a build without a profile to compare against, and report anything that miscompiles.
Install
tar xzf cruciblevm-0.4.0-linux-x86_64.tar.gz
cruciblevm/crucible/pgo.sh --helpThe archive is a GraalVM JDK with native-image and the CrucibleVM scripts; it needs neither mx nor the source tree. native-image needs a C toolchain: gcc, glibc-devel and zlib-devel on Fedora or Rocky, gcc libc6-dev zlib1g-dev on Debian or Ubuntu; the profile scripts need python3. Built on Rocky Linux 10.
Build an optimized executable in one command
cruciblevm/crucible/pgo.sh --name app --run '$APP <your workload arguments>' -- -cp app.jar com.example.MainIt builds an image that records, runs your workload on it, builds an image that samples call stacks, runs the workload again, and builds ./app, the image to ship. The arguments after -- go to native-image as they are. For a service, --run is a script that starts the image, puts load on it and stops it with SIGTERM (build with --install-exit-handlers). --no-samples stops after the counted profile. The steps one at a time, merging the profiles of several runs, and converting an Oracle GraalVM .iprof are in the user guide.
New in 0.4.0
- The young generation is collected on several threads by default. 0.2.0 and 0.3.0 shipped this as the opt-in
-H:+ParallelYoungCollection; build with-H:-ParallelYoungCollectionto keep collections on one thread.-XX:ParallelYoungCollectionThreads=<n>sets the thread count at run time; the default is the processors the program may use, which in a container is its CPU limit (up to 9), and with one processor collections stay serial. On eight processors the fourteen Renaissance benchmarks run 3.6% faster than serial (geometric mean; scala-kmeans loses most, 1.6%), and level on two and on one; akka-uct runs 15% faster and peaks at 1.2 GB and not 1.9, scala-stm-bench7 and reactors 12% faster, scrabble 7%. The Quarkus and Spring services answer as many requests in the same memory. -XX:+SerialGCShrinkWhilePromoting, off by default, lets the young generation shrink for memory while objects are promoted, so it ends at the same size with or without-Xms. For a service with a capped heap, use it with-XX:SerialGCTimeRatio=19: under-Xmx512mSpring PetClinic answers 5,490 to 5,520 requests a second in 142 to 149 MB (Oracle's binary 4,460 in 198 to 204 MB, the default 5,690 to 5,800 in 237 to 242), and the Quarkus quickstart 15,760 to 15,940 in 113 to 118 MB (Oracle's 15,000 in 150 to 167, the default 14,450 to 14,640 in 86 to 88). Without a cap, ratio 19 makes programs peak at about twice their memory, so both stay off by default.
How it compares
Same machine, each binary checked for identical output, time of the profile-guided binary from each compiler; lower is better. Oracle GraalVM 25.0.4, which has Oracle's PGO.
| workload | Oracle GraalVM | CrucibleVM |
|---|---|---|
| Renaissance, fourteen benchmarks, geometric mean of the time | 1 | 0.83 |
| GameOfLife (Oracle's own PGO example) | 3.39 s | 2.91 s |
| ArrayBench | 1.19 s | 0.31 s |
| BranchBench | 0.55 s | 0.54 s |
| JsonBench | 1.56 s | 1.39 s |
| BenchPGO | 0.37 s | 0.32 s |
Quarkus REST/JSON quickstart, requests a second on /fruits |
100,600 | 107,400 |
Spring PetClinic, requests a second on /vets (JSON) |
36,800 | 47,200 |
The fourteen Renaissance benchmarks: on eight processors with the parallel default, 0.832 of Oracle's time (0.864 serial). With the serial collector, ahead on thirteen of the fourteen and behind on rx-scrabble by 2 to 3%, which is the collector's sizing under that harness rather than the compiler; parallel collection costs rx-scrabble about 1% more. Spring PetClinic, built by Spring's own native build and measured over 90 s of load, answers 31% more requests a second than Oracle's profile-guided binary, Quarkus 3% fewer; Spring's memory under load is above Oracle's (see the shrink option above for a capped heap). Without a profile, at the default level, the fourteen run at 0.917 of Oracle's default binary. The measurements, the method and everything tried that did not work are in crucible/README.md, crucible/ROADMAP.md and docs/issues/.
Known limits
- Linux x86-64 only; nothing else has been built or tested.
- akka-uct can hang, about once in two thousand runs: that is akka's own fork-join bug (akka/akka-core#33019), and it hangs the same way on HotSpot.
- The recording image is larger and slower than Oracle's: 100 to 140 MB where Oracle's is 50 to 65, and scrabble records at 3.2 s an iteration where Oracle's recording image takes 2.4.
- A type the workload used without locking and production locks heavily is locked slowly; record a workload that locks what production locks.
- akka-uct peaks in about 2 GB where Oracle's binary peaks in 1; the policy grows the young generation to 1 GB because the program spends half its time in young collections at any size. Capped at
-Xmx1git runs 5% slower in 1.05 GB and at-Xmx700m10% slower in 0.72 GB, still 20% and 15% ahead of Oracle's binary; cap such a program by what it may take.
Checksum
4ce58635378b2cf25d09cf1c1eeb845ffe06b23c39da0493b01cc815b8aaf773 cruciblevm-0.4.0-linux-x86_64.tar.gz