Skip to content

Commit e9b3d5e

Browse files
dfa1claude
andcommitted
test(benchmark): add golden-corpus throughput benchmark vs zstd-jni
Benchmarks compress/decompress on zstd's own vendored golden corpus (third_party/zstd/tests/golden-compression) — small, structurally varied real fixtures (143 B to 256 KiB) rather than the synthetic ~3x payloads of CompressBenchmark/DecompressBenchmark. These exercise per-call native-boundary overhead, where FFM-vs-JNI fixed costs show up: the zero-copy MemorySegment path beats zstd-jni on every file, by the widest margin on the smallest. Covers zstd-java byte[] and MemorySegment modes against zstd-jni, one @Param per corpus file; resolves the corpus by walking up to third_party/zstd/tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ea1ac84 commit e9b3d5e

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package io.github.dfa1.zstd.bench;
2+
3+
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
4+
5+
import io.github.dfa1.zstd.Zstd;
6+
import io.github.dfa1.zstd.ZstdCompressCtx;
7+
import io.github.dfa1.zstd.ZstdDecompressCtx;
8+
import java.io.UncheckedIOException;
9+
import java.io.IOException;
10+
import java.lang.foreign.Arena;
11+
import java.lang.foreign.MemorySegment;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.util.concurrent.TimeUnit;
15+
import org.openjdk.jmh.annotations.Benchmark;
16+
import org.openjdk.jmh.annotations.BenchmarkMode;
17+
import org.openjdk.jmh.annotations.Fork;
18+
import org.openjdk.jmh.annotations.Level;
19+
import org.openjdk.jmh.annotations.Measurement;
20+
import org.openjdk.jmh.annotations.Mode;
21+
import org.openjdk.jmh.annotations.OutputTimeUnit;
22+
import org.openjdk.jmh.annotations.Param;
23+
import org.openjdk.jmh.annotations.Scope;
24+
import org.openjdk.jmh.annotations.Setup;
25+
import org.openjdk.jmh.annotations.State;
26+
import org.openjdk.jmh.annotations.TearDown;
27+
import org.openjdk.jmh.annotations.Warmup;
28+
29+
/// Throughput on zstd's own vendored golden corpus — the real, version-matched
30+
/// fixtures under `third_party/zstd/tests/golden-compression`, rather than the
31+
/// synthetic ~3x-compressible payloads of [CompressBenchmark] / [DecompressBenchmark].
32+
///
33+
/// These files are small (143 B to 256 KiB) and structurally varied (an HTTP
34+
/// header, a pathological Huffman case, long literal/match runs, a block-splitter
35+
/// regression), so they exercise the per-call native-boundary overhead far more
36+
/// than the bandwidth-bound synthetic 64 MiB case. This is where FFM-vs-JNI
37+
/// fixed costs actually show up. Each `@Param` is one corpus file.
38+
@BenchmarkMode(Mode.Throughput)
39+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
40+
@State(Scope.Thread)
41+
@Fork(value = 1, jvmArgsAppend = "--enable-native-access=ALL-UNNAMED")
42+
@Warmup(iterations = 3)
43+
@Measurement(iterations = 5)
44+
public class GoldenCorpusBenchmark {
45+
46+
@Param({
47+
"http",
48+
"huffman-compressed-larger",
49+
"large-literal-and-match-lengths",
50+
"PR-3517-block-splitter-corruption-test",
51+
})
52+
private String file;
53+
54+
@Param({"3"})
55+
private int level;
56+
57+
private byte[] src;
58+
private int srcSize;
59+
private byte[] frame;
60+
61+
private ZstdCompressCtx cctx;
62+
private ZstdDecompressCtx dctx;
63+
private byte[] compressDst;
64+
65+
private Arena arena;
66+
private MemorySegment srcSeg;
67+
private MemorySegment frameSeg;
68+
private MemorySegment compressDstSeg;
69+
private MemorySegment decompressDstSeg;
70+
71+
@Setup(Level.Trial)
72+
public void setup() {
73+
src = read(corpus().resolve("golden-compression").resolve(file));
74+
srcSize = src.length;
75+
frame = Zstd.compress(src);
76+
77+
cctx = new ZstdCompressCtx().level(level);
78+
dctx = new ZstdDecompressCtx();
79+
int bound = (int) Zstd.compressBound(srcSize);
80+
compressDst = new byte[bound];
81+
82+
arena = Arena.ofConfined();
83+
srcSeg = arena.allocate(srcSize);
84+
MemorySegment.copy(src, 0, srcSeg, JAVA_BYTE, 0, srcSize);
85+
frameSeg = arena.allocate(frame.length);
86+
MemorySegment.copy(frame, 0, frameSeg, JAVA_BYTE, 0, frame.length);
87+
compressDstSeg = arena.allocate(bound);
88+
decompressDstSeg = arena.allocate(srcSize);
89+
}
90+
91+
@TearDown(Level.Trial)
92+
public void tearDown() {
93+
cctx.close();
94+
dctx.close();
95+
arena.close();
96+
}
97+
98+
@Benchmark
99+
public byte[] compressJavaBytes() {
100+
return cctx.compress(src);
101+
}
102+
103+
@Benchmark
104+
public long compressJavaSegment() {
105+
return cctx.compress(compressDstSeg, srcSeg);
106+
}
107+
108+
@Benchmark
109+
public byte[] compressJni() {
110+
return com.github.luben.zstd.Zstd.compress(src, level);
111+
}
112+
113+
@Benchmark
114+
public byte[] decompressJavaBytes() {
115+
return dctx.decompress(frame, srcSize);
116+
}
117+
118+
@Benchmark
119+
public long decompressJavaSegment() {
120+
return dctx.decompress(decompressDstSeg, frameSeg);
121+
}
122+
123+
@Benchmark
124+
public byte[] decompressJni() {
125+
return com.github.luben.zstd.Zstd.decompress(frame, srcSize);
126+
}
127+
128+
private static byte[] read(Path file) {
129+
try {
130+
return Files.readAllBytes(file);
131+
} catch (IOException e) {
132+
throw new UncheckedIOException("cannot read corpus file " + file, e);
133+
}
134+
}
135+
136+
/// Walks up from the working directory to find `third_party/zstd/tests`, the
137+
/// vendored corpus shipped via the `third_party/zstd` git submodule.
138+
private static Path corpus() {
139+
Path dir = Path.of("").toAbsolutePath();
140+
while (dir != null) {
141+
Path candidate = dir.resolve("third_party/zstd/tests");
142+
if (Files.isDirectory(candidate)) {
143+
return candidate;
144+
}
145+
dir = dir.getParent();
146+
}
147+
throw new IllegalStateException(
148+
"golden corpus not found: third_party/zstd/tests is missing — "
149+
+ "run `git submodule update --init --recursive`");
150+
}
151+
}

0 commit comments

Comments
 (0)