Skip to content

Commit 1b8edbf

Browse files
dfa1claude
andcommitted
docs: add async-profiler analysis of 64 MiB case
Alloc + itimer flamegraphs confirm the segment path allocates nothing and skips the memcpy bounce + GC the byte[]/JNI paths pay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1352e63 commit 1b8edbf

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

docs/benchmarks.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,39 @@ scale linearly with payload size.
9999
regardless of binding; the segment path additionally avoids the off-heap↔heap copy.
100100
aircompressor *compress* still allocates internal hash tables per call.
101101

102+
## Where the work goes (async-profiler, 64 MiB)
103+
104+
Profiling the cache-busting 64 MiB case with async-profiler corroborates the
105+
allocation counters from two angles — allocation flamegraph (what hits the heap)
106+
and itimer/CPU flamegraph (what burns cycles).
107+
108+
**Allocation flamegraph** — dominant heap-allocation site per benchmark:
109+
110+
| benchmark | dominant alloc site |
111+
|-----------|---------------------|
112+
| Compress `zstdJavaSegment` | **none — no heap allocation sampled** |
113+
| Compress `zstdJavaBytes` | `byte[]` in `Zstd.copyOut` (the returned frame array) |
114+
| Compress `zstdJni` | `byte[]` inside `luben…Zstd.compress` |
115+
| Compress `aircompressor` | `byte[]` + `BlockCompressionState.<init>` (internal tables) |
116+
| Decompress `zstdJavaSegment` | **none — no heap allocation sampled** |
117+
| Decompress `zstdJavaBytes` / `zstdJni` | `byte[]` (the returned output array) |
118+
119+
**CPU (itimer) flamegraph** — the heap paths additionally pay a memcpy bounce and
120+
GC work that the segment path does not:
121+
122+
- `zstdJavaBytes`: `MemorySegment.copy``ScopedMemoryAccess.copyMemory`
123+
`Unsafe.copyMemory` (heap↔native in/out), **plus** `G1ParCopyClosure::do_oop_work`
124+
(GC triggered by the output allocation).
125+
- `zstdJni`: `byte_disjoint_arraycopy` + `Arrays.copyOfRange` (the JNI copy and the
126+
trim copy).
127+
- `zstdJavaSegment`: **neither** — no copy frames, no GC frames. Only codec work
128+
(`ZSTD_*`, `FSE_buildCTable_wksp`, `encodeSequences`, `FSE_readNCount`).
129+
130+
So the segment API does strictly less work: no per-op heap allocation (hence no GC)
131+
and no memcpy bounce. At 64 MiB this overhead is a small fraction of total codec
132+
time — which is why throughput ties — but it is pure waste the zero-copy path
133+
removes entirely, and it dominates under sustained, allocation-sensitive load.
134+
102135
## Reproduce
103136

104137
```bash
@@ -109,4 +142,11 @@ java -jar benchmark/target/benchmarks.jar -prof gc
109142

110143
# single size
111144
java -jar benchmark/target/benchmarks.jar -prof gc -p size=67108864
145+
146+
# async-profiler flamegraphs for the 64 MiB case (macOS: itimer; Linux: cpu)
147+
LIB=/opt/homebrew/lib/libasyncProfiler.dylib
148+
java -jar benchmark/target/benchmarks.jar -p size=67108864 \
149+
-prof "async:libPath=$LIB;output=flamegraph;event=alloc;dir=benchmark/target/async-alloc"
150+
java -jar benchmark/target/benchmarks.jar -p size=67108864 \
151+
-prof "async:libPath=$LIB;output=flamegraph;event=itimer;dir=benchmark/target/async-cpu"
112152
```

0 commit comments

Comments
 (0)