Skip to content

Commit bd5a84e

Browse files
committed
readme/bench: re-measure everything, and move workload 2 to Java 28
The performance tables were from the pre-mimalloc build and workload 2 was the JDK 26 image — stale on both counts. Re-ran the whole matrix in one session on the same host (all tools on OpenJDK 27, -Xmx8g), and the script now targets the NEWEST JDK's module image (28 EA, 27,766 entries -> 15,039 units): rt.jar (12,609 units) jcdc 3.6 s / 684 MB (single-thread 12.0 s / 458 MB) Vineflower 24.7 s, CFR 51.9 s, Procyon 108.8 s, fernflower 208.6 s (2.2-8.6 GB peak) Java 28 image (15,039) jcdc 6.2 s / 945 MB (single-thread 31.6 s / 568 MB) CFR 144.7 s, fernflower 497.5 s, Procyon 725.2 s, Vineflower: cannot finish jcdc is ~6.8x Vineflower, ~14x CFR, ~30x Procyon and ~57x fernflower on rt.jar, and ~23x / ~80x / ~117x on the Java 28 image; the peak-RSS gap is 2.3x-19x. Even single-threaded it beats every JVM tool here. The Vineflower footnote was re-probed rather than carried over: at 8 g it catches 194 per-class `Java heap space` errors and writes nothing in 300 s; at 16 g the heap holds but it still writes nothing in 7 minutes (>= 9 GB RSS); the 32 g / 40-minute data point is the earlier probe on a same-size image. Verified the harness itself while doing this: the four tool jars are back in /tmp/bench_tools (procyon 2,004,704 bytes = the same release asset as before) and `bench.sh` points at corpus/jdks/jdk28.
1 parent a8658e2 commit bd5a84e

3 files changed

Lines changed: 77 additions & 73 deletions

File tree

‎README.md‎

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,56 +36,59 @@ fresh Rust implementation.
3636

3737
Whole-SDK benchmarks on two workloads — every class in **Java 8's `rt.jar`**
3838
(20,413 class entries → 12,609 emitted units) and **every class of the newest
39-
JDK, Java 26**. (JDK 9+ has no `rt.jar`: the boot classpath became the module
40-
image `lib/modules`, so workload 2 is that image's complete class set — all
41-
68 modules, 27,855 entries → 15,231 units — extracted with `jimage`.) Host:
42-
Apple M5 Pro (18 cores, 48 GB), macOS. All Java tools run on the same JVM
43-
(OpenJDK 26) with a uniform `-Xmx8g` heap; each tool gets one
39+
JDK, Java 28 (early access)**. (JDK 9+ has no `rt.jar`: the boot classpath
40+
became the module image `lib/modules`, so workload 2 is that image's complete
41+
class set — all 68 modules, 27,766 entries → 15,039 units — extracted with
42+
`jimage`.) Host: Apple M5 Pro (18 cores, 48 GB), macOS. All Java tools run on
43+
the same JVM (OpenJDK 27) with a uniform `-Xmx8g` heap; each tool gets one
4444
`/usr/bin/time -l` run (wall time + peak RSS) after jcdc has warmed the page
4545
cache. Reproduce with [`scripts/bench.sh`](scripts/bench.sh).
4646

4747
**Workload 1 — `rt.jar` (JDK 8, 12,609 units):**
4848

4949
| Tool | Wall time | Peak RSS | Units emitted |
5050
|---|---:|---:|---:|
51-
| **jcdc** (parallel — default, 1 worker/core) | **6.0 s** | **788 MB** | 12,609 |
52-
| jcdc (`JCDC_THREADS=1`, single-threaded) | 24.1 s | 493 MB | 12,609 |
53-
| Vineflower 1.11.1 | 38.5 s | 8,765 MB | 12,609 |
54-
| CFR 0.152 | 87.3 s | 4,242 MB | 12,609 |
55-
| Procyon 0.6.0 | 172.8 s | 2,282 MB | 12,586 |
56-
| fernflower (JetBrains) | 297.2 s | 2,438 MB | 12,609 |
51+
| **jcdc** (parallel — default, 1 worker/core) | **3.6 s** | **684 MB** | 12,609 |
52+
| jcdc (`JCDC_THREADS=1`, single-threaded) | 12.0 s | 458 MB | 12,609 |
53+
| Vineflower 1.11.1 | 24.7 s | 8,633 MB | 12,609 |
54+
| CFR 0.152 | 51.9 s | 4,516 MB | 12,609 |
55+
| Procyon 0.6.0 | 108.8 s | 2,218 MB | 12,586 |
56+
| fernflower (JetBrains) | 208.6 s | 2,639 MB | 12,609 |
5757

58-
**Workload 2 — JDK 26 runtime image (`lib/modules`, 15,231 units):**
58+
**Workload 2 — Java 28 runtime image (`lib/modules`, 15,039 units):**
5959

6060
| Tool | Wall time | Peak RSS | Units emitted |
6161
|---|---:|---:|---:|
62-
| **jcdc** (parallel — default) | **17.9 s** | **1,052 MB** | 15,231 |
63-
| jcdc (`JCDC_THREADS=1`, single-threaded) | 68.0 s | 672 MB | 15,231 |
64-
| CFR 0.152 | 120.8 s | 8,171 MB | 15,235 |
65-
| fernflower (JetBrains) | 409.5 s | 6,082 MB | 15,231 |
66-
| Procyon 0.6.0 | 623.2 s | 4,538 MB | 15,233 |
67-
| Vineflower 1.11.1 † | capped at 300 s | 8,821 MB | 0 |
68-
69-
† Vineflower **cannot finish this workload**: `scripts/bench.sh` climbs a heap
70-
ladder (8 g → 16 g → 32 g) precisely because this image defeats it — at 8 g and
71-
16 g it catches hundreds of per-class `Java heap space` errors and writes
72-
nothing, and at 32 g it was still running at a 40-minute cap (16.9 GB peak RSS,
73-
zero files). It does finish the core module alone (`java.base`, 7,422 entries)
74-
with a 16 g heap in 41.5 s — still slower than jcdc's whole-image run.
62+
| **jcdc** (parallel — default) | **6.2 s** | **945 MB** | 15,039 |
63+
| jcdc (`JCDC_THREADS=1`, single-threaded) | 31.6 s | 568 MB | 15,039 |
64+
| CFR 0.152 | 144.7 s | 8,245 MB | 15,012 |
65+
| fernflower (JetBrains) | 497.5 s | 5,108 MB | 15,039 |
66+
| Procyon 0.6.0 | 725.2 s | 6,232 MB | 15,011 |
67+
| Vineflower 1.11.1 † | capped at 300 s | 8,707 MB | 0 |
68+
69+
† Vineflower **cannot finish this workload**, which is why `scripts/bench.sh`
70+
climbs a heap ladder (8 g → 16 g → 32 g): at **8 g** it catches 194 per-class
71+
`Java heap space` errors and writes no file in 300 s; at **16 g** the heap
72+
holds but it still writes nothing in 7 minutes (≥ 9 GB RSS); a previous **32 g**
73+
probe on a same-size image was still running at the 40-minute cap (16.9 GB
74+
peak, zero files). It does finish the core module alone (`java.base`, 7,422
75+
entries) with a 16 g heap in 41.5 s — still slower than jcdc's whole-image
76+
run.
7577

7678
Even **single-threaded**, jcdc finishes both workloads faster than every JVM
77-
tool here (24.1 s vs Vineflower's 38.5 s on `rt.jar`), and its whole-run peak
78-
RSS (**493–1,052 MB** against **2.3–8.8 GB** for the JVM tools) is 2.2×–17.8×
79-
smaller. With its default parallel pipeline it is ~6.4× faster than
80-
Vineflower, ~14× CFR, ~29× Procyon and ~49× fernflower on `rt.jar`, and ~6.7×
81-
(CFR) / ~23× (fernflower) / ~35× (Procyon) faster on the JDK 26 image.
82-
Emitted-unit counts differ slightly between tools: jcdc folds `$`-named hidden
83-
classes into their family file (and emits `module-info.java`), while Procyon
84-
skips `package-info` files (−23 on `rt.jar`). (jcdc runs with `-cp <jar>` for
85-
full type context; the JVM tools resolve from the input jar itself.)
79+
tool here (12.0 s vs Vineflower's 24.7 s on `rt.jar`), and its whole-run peak
80+
RSS (**458–945 MB** against **2.2–8.7 GB** for the JVM tools) is 2.3×–19×
81+
smaller. With its default parallel pipeline it is ~6.8× faster than
82+
Vineflower, ~14× CFR, ~30× Procyon and ~57× fernflower on `rt.jar`, and
83+
~23× (CFR) / ~80× (fernflower) / ~117× (Procyon) faster on the Java 28
84+
image. Emitted-unit counts differ slightly between tools: jcdc folds `$`-named
85+
hidden classes into their family file (and emits `module-info.java`), while
86+
Procyon skips `package-info` files (−23 on `rt.jar`). (jcdc runs with
87+
`-cp <jar>` for full type context; the JVM tools resolve from the input jar
88+
itself.)
8689

8790
Times move a few tens of percent between sessions on this machine; the rows in
88-
each table are from one run (2026-09-15, jdc-core-based jcdc), so the ratios
91+
each table are from one run (2026-10-24, mimalloc-based jcdc), so the ratios
8992
are what matters.
9093

9194
## Install

‎README.zh.md‎

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,54 +31,55 @@ Java 源码。设计参考了 fernflower/Vineflower、garlic、CFR、Procyon、K
3131
## 性能对比
3232

3333
两组整包 SDK 基准——**Java 8 `rt.jar`** 全量 class(20,413 个 class 条目 →
34-
12,609 个编译单元)与**最新 JDK(Java 26)的全部 class**。(JDK 9 起
34+
12,609 个编译单元)与**最新 JDK(Java 28 早期访问版)的全部 class**。(JDK 9 起
3535
`rt.jar` 已不存在:boot classpath 变成了模块镜像 `lib/modules`,因此负载 2
36-
取该镜像的完整 class 集合——全部 68 个模块、27,855 个条目 → 15,231 个编译
36+
取该镜像的完整 class 集合——全部 68 个模块、27,766 个条目 → 15,039 个编译
3737
单元,用 `jimage` 提取。)测试机:Apple M5 Pro(18 核 / 48GB)/ macOS。
38-
所有 Java 工具统一跑在同一 JVM(OpenJDK 26)、统一 `-Xmx8g` 堆;jcdc 先跑
38+
所有 Java 工具统一跑在同一 JVM(OpenJDK 27)、统一 `-Xmx8g` 堆;jcdc 先跑
3939
预热页缓存,之后每个工具 `/usr/bin/time -l` 计时一次(墙钟 + 峰值 RSS)。
4040
复现脚本:[`scripts/bench.sh`](scripts/bench.sh)。
4141

4242
**负载 1 —— `rt.jar`(JDK 8,12,609 个编译单元):**
4343

4444
| 工具 | 墙钟时间 | 峰值内存 | 产出文件 |
4545
|---|---:|---:|---:|
46-
| **jcdc**(并行——默认,每核 1 worker) | **6.0 秒** | **788 MB** | 12,609 |
47-
| jcdc(`JCDC_THREADS=1` 单线程) | 24.1 秒 | 493 MB | 12,609 |
48-
| Vineflower 1.11.1 | 38.5 秒 | 8,765 MB | 12,609 |
49-
| CFR 0.152 | 87.3 秒 | 4,242 MB | 12,609 |
50-
| Procyon 0.6.0 | 172.8 秒 | 2,282 MB | 12,586 |
51-
| fernflower(JetBrains) | 297.2 秒 | 2,438 MB | 12,609 |
46+
| **jcdc**(并行——默认,每核 1 worker) | **3.6 秒** | **684 MB** | 12,609 |
47+
| jcdc(`JCDC_THREADS=1` 单线程) | 12.0 秒 | 458 MB | 12,609 |
48+
| Vineflower 1.11.1 | 24.7 秒 | 8,633 MB | 12,609 |
49+
| CFR 0.152 | 51.9 秒 | 4,516 MB | 12,609 |
50+
| Procyon 0.6.0 | 108.8 秒 | 2,218 MB | 12,586 |
51+
| fernflower(JetBrains) | 208.6 秒 | 2,639 MB | 12,609 |
5252

53-
**负载 2 —— JDK 26 运行时镜像(`lib/modules`,15,231 个编译单元):**
53+
**负载 2 —— Java 28 运行时镜像(`lib/modules`,15,039 个编译单元):**
5454

5555
| 工具 | 墙钟时间 | 峰值内存 | 产出文件 |
5656
|---|---:|---:|---:|
57-
| **jcdc**(并行——默认) | **17.9 秒** | **1,052 MB** | 15,231 |
58-
| jcdc(`JCDC_THREADS=1` 单线程) | 68.0 秒 | 672 MB | 15,231 |
59-
| CFR 0.152 | 120.8 秒 | 8,171 MB | 15,235 |
60-
| fernflower(JetBrains) | 409.5 秒 | 6,082 MB | 15,231 |
61-
| Procyon 0.6.0 | 623.2 秒 | 4,538 MB | 15,233 |
62-
| Vineflower 1.11.1 † | 300 秒时封顶 | 8,821 MB | 0 |
63-
64-
† Vineflower **跑不完这组负载**:`scripts/bench.sh` 之所以要爬堆阶梯
65-
(8g → 16g → 32g)就是被这组负载逼出来的——8g/16g 下逐类捕获 `Java heap
66-
space`、一个文件都写不出,32g 跑满 40 分钟封顶时仍在运行(峰值 16.9 GB、
67-
0 个文件)。它单独跑核心模块 `java.base`(7,422 个条目)可以完成:16g 堆
68-
41.5 秒——但仍慢于 jcdc 跑完整镜像。
57+
| **jcdc**(并行——默认) | **6.2 秒** | **945 MB** | 15,039 |
58+
| jcdc(`JCDC_THREADS=1` 单线程) | 31.6 秒 | 568 MB | 15,039 |
59+
| CFR 0.152 | 144.7 秒 | 8,245 MB | 15,012 |
60+
| fernflower(JetBrains) | 497.5 秒 | 5,108 MB | 15,039 |
61+
| Procyon 0.6.0 | 725.2 秒 | 6,232 MB | 15,011 |
62+
| Vineflower 1.11.1 † | 300 秒时封顶 | 8,707 MB | 0 |
63+
64+
† Vineflower **跑不完这组负载**,`scripts/bench.sh` 的堆阶梯(8g → 16g →
65+
32g)就是为它设的:**8g** 下逐类捕获 194 次 `Java heap space`、300 秒内
66+
一个文件都没写出;**16g** 堆不再耗尽,但 7 分钟内依旧 0 文件(RSS ≥ 9 GB);
67+
早前在同一规模的镜像上试过 **32g**,跑满 40 分钟封顶时仍在运行(峰值
68+
16.9 GB、0 个文件)。它单独跑核心模块 `java.base`(7,422 个条目)可以完成:
69+
16g 堆 41.5 秒——但仍慢于 jcdc 跑完整镜像。
6970

7071
即便**单线程**,jcdc 跑完两组负载也比这里所有 Java 工具快(`rt.jar` 上
71-
24.1 秒 vs Vineflower 38.5 秒),整轮峰值内存(**493–1,052 MB**,对比
72-
Java 工具的 **2.3–8.8 GB**)低 2.2×–17.8×。默认并行管线下:`rt.jar` 上比
73-
Vineflower 快约 6.4 倍、比 CFR 快约 14 倍、比 Procyon 快约 29 倍、比
74-
fernflower 快约 49 倍;JDK 26 镜像上比 CFR 快约 6.7 倍、比 fernflower 快
75-
约 23 倍、比 Procyon 快约 35 倍。各工具产出文件数的细微差异来自归类口径:
72+
12.0 秒 vs Vineflower 24.7 秒),整轮峰值内存(**458–945 MB**,对比
73+
Java 工具的 **2.2–8.7 GB**)低 2.3×–19×。默认并行管线下:`rt.jar` 上比
74+
Vineflower 快约 6.8 倍、比 CFR 快约 14 倍、比 Procyon 快约 30 倍、比
75+
fernflower 快约 57 倍;Java 28 镜像上比 CFR 快约 23 倍、比 fernflower 快
76+
约 80 倍、比 Procyon 快约 117 倍。各工具产出文件数的细微差异来自归类口径:
7677
jcdc 把 `$` 命名的隐藏类折叠进所属族文件(并输出 `module-info.java`),
7778
Procyon 则跳过 `package-info` 文件(`rt.jar` 上少 23 个)。(jcdc 带
7879
`-cp <jar>` 提供完整类型上下文;Java 工具从输入 jar 自行解析。)
7980

8081
同一台机器不同时段跑,绝对秒数会有几十个百分点的波动;每张表内的数据取自
81-
同一次运行(2026-09-15,基于 jdc-core 的 jcdc),看比值更有意义。
82+
同一次运行(2026-10-24,采用 mimalloc 分配器的 jcdc),看比值更有意义。
8283

8384
## 安装
8485

‎scripts/bench.sh‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
#
66
# Workloads:
77
# 1. jdk8 rt.jar — 20,413 class entries (12,609 top-level units)
8-
# 2. jdk26 full image — every class of the newest JDK, i.e. the post-JDK8
8+
# 2. jdk28 full image — every class of the newest JDK, i.e. the post-JDK8
99
# successor of rt.jar (JDK 9+ has no rt.jar: the boot classpath moved
1010
# into the module image lib/modules). Build it with:
11-
# jdk26/bin/jimage extract --dir X jdk26/lib/modules
11+
# jdk28/bin/jimage extract --dir X jdk28/lib/modules
1212
# for m in X/*/; do rsync -a --exclude module-info.class "$m" Y/; done
13-
# (cd Y && jar cf jdk26-full.jar .) # 27,855 classes
13+
# (cd Y && jar cf jdk28-full.jar .) # 27,766 classes
1414
# (module-info.class is dropped: 68 modules share that one path.)
1515
#
1616
# Tools (jars in /tmp/bench_tools, or set BENCH_TOOLS):
@@ -23,7 +23,7 @@
2323
# /usr/bin/time -l (wall + peak RSS). jcdc additionally runs with
2424
# JCDC_THREADS=1 for an honest single-thread comparison. fernflower emits a
2525
# JAR of sources for jar input — it is unpacked (untimed) for counting.
26-
# Vineflower exhausts an 8g heap on the jdk26 image (it catches the per-class
26+
# Vineflower exhausts an 8g heap on the full JDK image (it catches the per-class
2727
# OutOfMemoryError and writes nothing): it is retried up a heap ladder until
2828
# a run logs no "Java heap space", and the row is labelled with the heap that
2929
# finally worked. Each attempt is capped (it otherwise thrashes for tens of
@@ -32,19 +32,19 @@
3232
# VINE_ATTEMPT_TIMEOUT=900 seconds per attempt (alarm(2) via perl)
3333
#
3434
# Set BENCH_WORKLOADS to run a subset, e.g.:
35-
# BENCH_WORKLOADS=jdk26-all-modules zsh scripts/bench.sh
35+
# BENCH_WORKLOADS=jdk28-all-modules zsh scripts/bench.sh
3636

3737
set -u
3838
TOOLS=${BENCH_TOOLS:-/tmp/bench_tools}
3939
JAVA=/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home/bin/java
4040
JCDC=/Users/e/Documents/project/jcdc/target/release/jcdc
4141
RT8=/Users/e/Documents/project/jcdc/corpus/jdks/jdk8/Contents/Home/jre/lib/rt.jar
42-
RT26=$TOOLS/jdk26-full.jar
42+
RT28=$TOOLS/jdk28-full.jar
4343
WORK=/tmp/bench_run
4444
XMX=-Xmx8g
4545

4646
echo "host: $(uname -m), $(sysctl -n hw.ncpu) cores, $(($(sysctl -n hw.memsize)/1073741824))GB"
47-
echo "inputs: rt.jar(jdk8) $(unzip -l $RT8 | grep -c '\.class$') classes; jdk26-full.jar $(unzip -l $RT26 | grep -c '\.class$') classes"
47+
echo "inputs: rt.jar(jdk8) $(unzip -l $RT8 | grep -c '\.class$') classes; jdk28-full.jar $(unzip -l $RT28 | grep -c '\.class$') classes"
4848
echo
4949

5050
report() { # label timefile outdir
@@ -67,7 +67,7 @@ bench_workload() { # workload-name input-jar
6767
/usr/bin/time -l env JCDC_THREADS=1 $JCDC -cp $IN $IN -o $WORK/$W-jcdc-seq > /dev/null 2> $WORK/$W-jcdc-seq.time
6868
report "jcdc (JCDC_THREADS=1)" $WORK/$W-jcdc-seq.time $WORK/$W-jcdc-seq
6969

70-
# Vineflower exhausts small heaps on the jdk26 image; climb the ladder
70+
# Vineflower exhausts small heaps on the full JDK image; climb the ladder
7171
# until a run logs no "Java heap space", and label the row.
7272
local vf_label="vineflower 1.11.1"
7373
local vf_cap=${VINE_ATTEMPT_TIMEOUT:-900}
@@ -94,10 +94,10 @@ bench_workload() { # workload-name input-jar
9494
report "procyon 0.6.0" $WORK/$W-procyon.time $WORK/$W-procyon
9595
}
9696

97-
for w in ${=BENCH_WORKLOADS:-rt-jar-jdk8 jdk26-all-modules}; do
97+
for w in ${=BENCH_WORKLOADS:-rt-jar-jdk8 jdk28-all-modules}; do
9898
case $w in
9999
rt-jar-jdk8) bench_workload rt-jar-jdk8 $RT8 ;;
100-
jdk26-all-modules) bench_workload jdk26-all-modules $RT26 ;;
100+
jdk28-all-modules) bench_workload jdk28-all-modules $RT28 ;;
101101
*) echo "unknown workload: $w" >&2; exit 1 ;;
102102
esac
103103
done

0 commit comments

Comments
 (0)