docs: consolidate benchmark.md into BENCHMARKS.md
- Merge libhdf5 1.14.6 head-to-head comparison from benchmark.md into BENCHMARKS.md h5bench section (sequential read/write, chunked write, metadata — attribute write, group create) - Recalculate speedup ratios using current clawhdf5 numbers (post auto-shuffle): chunked write 512×512 now 38.4× faster than libhdf5 (was 16×) - Fix groups_create/traverse column header mismatch: data was k=4/16/32/64 but labeled k=4/16/64/128; corrected with separate Groups table - Clarify Pcodec codec comparison benchmarked without auto-shuffle (shuffle degrades Pcodec which handles byte organization internally) - Add "vs libhdf5 Summary" and "Why the Gaps" interpretation sections - Delete benchmark.md (content fully absorbed) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c30ed0cda5
commit
1347746973
+117
-36
@@ -399,18 +399,30 @@ cargo run --release --bin ephemeral_perf
|
|||||||
|
|
||||||
## h5bench-Equivalent I/O Benchmarks
|
## h5bench-Equivalent I/O Benchmarks
|
||||||
|
|
||||||
Criterion harness mirroring h5bench serial workloads. Clawhdf5-only (no libhdf5 C library).
|
Criterion harness mirroring h5bench serial workloads. clawhdf5 benchmarks dated 2026-07-01;
|
||||||
**Run:** `cargo bench -p clawhdf5-bench`
|
libhdf5 1.14.6 head-to-head comparison dated 2026-06-30 (same hardware, same Criterion harness).
|
||||||
**Date:** 2026-07-01 · same system as above · post write-performance improvements (chunk-cache, SIMD shuffle, Zstd codec, owned-Vec IO, auto-shuffle pre-filter, Pcodec codec).
|
|
||||||
|
```bash
|
||||||
|
cargo bench -p clawhdf5-bench # clawhdf5-only
|
||||||
|
cargo bench -p clawhdf5-bench --features libhdf5-compare # head-to-head
|
||||||
|
```
|
||||||
|
|
||||||
### Sequential Read Throughput
|
### Sequential Read Throughput
|
||||||
|
|
||||||
|
Both read a 1-D contiguous f32 dataset. clawhdf5 parses from `Vec<u8>` (zero-copy);
|
||||||
|
libhdf5 reads from a temp file including `open` + `read` + `close` overhead.
|
||||||
|
|
||||||
| Workload | n=1K | n=10K | n=100K |
|
| Workload | n=1K | n=10K | n=100K |
|
||||||
|----------|------|-------|--------|
|
|----------|------|-------|--------|
|
||||||
| read_sequential (f32) | 634 ns / **5.9 GiB/s** | 2.44 µs / **15.3 GiB/s** | 24.5 µs / **15.2 GiB/s** |
|
| **clawhdf5** f32 | 634 ns / **5.9 GiB/s** | 2.44 µs / **15.3 GiB/s** | 24.5 µs / **15.2 GiB/s** |
|
||||||
| read_f64_sequential (f64) | 743 ns / **10.0 GiB/s** | 4.17 µs / **17.8 GiB/s** | 43.3 µs / **17.2 GiB/s** |
|
| libhdf5 f32 | 45.2 µs / 85 MiB/s | 47.8 µs / 799 MiB/s | 73.9 µs / 5.0 GiB/s |
|
||||||
| read_from_disk (f64, OS I/O) | — | 10.1 µs / **7.4 GiB/s** | 77.6 µs / **9.6 GiB/s** |
|
| **Speedup** | **71×** | **20×** | **3.0×** |
|
||||||
| read_hyperslab (f64, 10% slice) | — | 4.09 µs / **1.8 GiB/s** | 50.1 µs / **1.5 GiB/s** |
|
| clawhdf5 f64 | 743 ns / **10.0 GiB/s** | 4.17 µs / **17.8 GiB/s** | 43.3 µs / **17.2 GiB/s** |
|
||||||
|
| clawhdf5 from_disk (f64, OS I/O) | — | 10.1 µs / **7.4 GiB/s** | 77.6 µs / **9.6 GiB/s** |
|
||||||
|
| clawhdf5 hyperslab (f64, 10% slice) | — | 4.09 µs / **1.8 GiB/s** | 50.1 µs / **1.5 GiB/s** |
|
||||||
|
|
||||||
|
libhdf5 f64 comparison excluded — clawhdf5's datatype encoding differs from libhdf5's (known
|
||||||
|
gap), making cross-format reads unreliable for comparison.
|
||||||
|
|
||||||
### Chunked Read Throughput
|
### Chunked Read Throughput
|
||||||
|
|
||||||
@@ -422,16 +434,20 @@ Criterion harness mirroring h5bench serial workloads. Clawhdf5-only (no libhdf5
|
|||||||
|
|
||||||
### Sequential Write Throughput
|
### Sequential Write Throughput
|
||||||
|
|
||||||
|
Both write to disk. At 100K elements both converge on the OS `write()` syscall ceiling.
|
||||||
|
|
||||||
| Workload | n=1K | n=10K | n=100K |
|
| Workload | n=1K | n=10K | n=100K |
|
||||||
|----------|------|-------|--------|
|
|----------|------|-------|--------|
|
||||||
| write_1d_contiguous (f32) | 9.44 µs / **404 MiB/s** | 25 µs / **1.49 GiB/s** | 228 µs / **1.63 GiB/s** |
|
| **clawhdf5** f32 | 9.44 µs / **404 MiB/s** | 25 µs / **1.49 GiB/s** | 228 µs / **1.63 GiB/s** |
|
||||||
| write_f64_batch (f64 embeddings) | 6.50 µs (n=128) | 8.67 µs (n=512) / **450 MiB/s** | 10.27 µs (n=1K) / **761 MiB/s** |
|
| libhdf5 f32 | 77.9 µs / 49 MiB/s | 87.8 µs / 435 MiB/s | 214 µs / 1.74 GiB/s |
|
||||||
|
| **Speedup** | **8.2×** | **3.5×** | **≈ tie** |
|
||||||
|
| clawhdf5 f64 embeddings | 6.50 µs (n=128) | 8.67 µs (n=512) / **450 MiB/s** | 10.27 µs (n=1K) / **761 MiB/s** |
|
||||||
|
|
||||||
### Chunked Write: Codec Comparison (Zstd-3 vs Deflate-6, with auto-shuffle)
|
### Chunked Write: Codec Comparison (with auto-shuffle)
|
||||||
|
|
||||||
Auto-shuffle is applied before all compression codecs by default (matches h5py behavior,
|
Auto-shuffle is applied before all compression codecs by default — AoS→SoA byte transpose,
|
||||||
implements byte-grouping pre-filter per arXiv:2506.18062). Shuffle dramatically improves
|
implements byte-grouping pre-filter per arXiv:2506.18062. Shuffle dramatically improves
|
||||||
both throughput and compression ratio for float/int data.
|
throughput for float/int data by creating long runs of similar bytes.
|
||||||
|
|
||||||
| Matrix size | Zstd-3 + shuffle | Deflate-6 + shuffle | Speedup |
|
| Matrix size | Zstd-3 + shuffle | Deflate-6 + shuffle | Speedup |
|
||||||
|-------------|-----------------|---------------------|---------|
|
|-------------|-----------------|---------------------|---------|
|
||||||
@@ -439,7 +455,7 @@ both throughput and compression ratio for float/int data.
|
|||||||
| 128×128 f32 | **148 µs / 422 MiB/s** | 153 µs / **407 MiB/s** | Parity |
|
| 128×128 f32 | **148 µs / 422 MiB/s** | 153 µs / **407 MiB/s** | Parity |
|
||||||
| 512×512 f32 | **1.34 ms / 748 MiB/s** | 1.39 ms / **719 MiB/s** | Zstd 1.04× faster |
|
| 512×512 f32 | **1.34 ms / 748 MiB/s** | 1.39 ms / **719 MiB/s** | Zstd 1.04× faster |
|
||||||
|
|
||||||
**Impact of auto-shuffle** (vs baseline without shuffle):
|
Impact of auto-shuffle vs no-shuffle baseline:
|
||||||
|
|
||||||
| Matrix size | Zstd-3 speedup | Deflate-6 speedup |
|
| Matrix size | Zstd-3 speedup | Deflate-6 speedup |
|
||||||
|-------------|----------------|-------------------|
|
|-------------|----------------|-------------------|
|
||||||
@@ -447,39 +463,104 @@ both throughput and compression ratio for float/int data.
|
|||||||
| 128×128 | +25% | **+204%** |
|
| 128×128 | +25% | **+204%** |
|
||||||
| 512×512 | +25% | **+157%** |
|
| 512×512 | +25% | **+157%** |
|
||||||
|
|
||||||
Both codecs now perform at parity at large sizes (~720–750 MiB/s). The shuffle filter
|
Both codecs perform at parity at large sizes (~720–750 MiB/s). Use `.with_zstd(3)` or
|
||||||
reorganizes bytes across all elements (AoS→SoA), creating long runs of similar bytes
|
`.with_deflate(6)` for write-heavy workloads. Use `.without_shuffle()` only for byte arrays
|
||||||
that both Zstd and deflate compress in fewer cycles.
|
or data that doesn't benefit from AoS→SoA transposition.
|
||||||
|
|
||||||
**Recommendation:** `.with_zstd(3)` or `.with_deflate(6)` — both are now competitive.
|
### Chunked Write vs libhdf5 (deflate-6)
|
||||||
Use `.without_shuffle()` only for byte arrays or data that does not benefit from AoS→SoA
|
|
||||||
transposition.
|
clawhdf5 compresses all chunks in memory and issues a single `write()`. libhdf5 flushes each
|
||||||
|
chunk individually via its Virtual File Layer (one `pwrite()` per chunk).
|
||||||
|
|
||||||
|
| Matrix | clawhdf5 deflate-6 + shuffle | libhdf5 deflate-6 | Speedup |
|
||||||
|
|--------|------------------------------|-------------------|---------|
|
||||||
|
| 32×32 f32 | 39 µs / 100 MiB/s | 172 µs / 23 MiB/s | **4.4×** |
|
||||||
|
| 128×128 f32 | 153 µs / 407 MiB/s | 3,150 µs / 20 MiB/s | **20.6×** |
|
||||||
|
| 512×512 f32 | 1,390 µs / 719 MiB/s | 53,300 µs / 19 MiB/s | **38.4×** |
|
||||||
|
|
||||||
|
The 32×32 speedup (4.4×) is lower than the 512×512 speedup (38.4×) because shuffle adds
|
||||||
|
overhead that dominates at 4 KB chunks. libhdf5 was benchmarked without shuffle. The speedup
|
||||||
|
compounds with matrix size because libhdf5's per-chunk VFL overhead is proportional to chunk
|
||||||
|
count while clawhdf5's single-pass cost is constant.
|
||||||
|
|
||||||
### Codec Comparison: Pcodec vs Zstd-3
|
### Codec Comparison: Pcodec vs Zstd-3
|
||||||
|
|
||||||
Pcodec (arXiv:2502.06112) is a pure-Rust lossless numerical codec that achieves 30–94% better
|
Pcodec (arXiv:2502.06112) is a pure-Rust lossless numerical codec with 30–94% better compression
|
||||||
compression ratio than Zstd for f32/f64 columns at 1–5 GiB/s decompression. Write throughput
|
ratio than Zstd for f32/f64 columns. Both sides benchmarked **without** auto-shuffle here (shuffle
|
||||||
comparison (same f32 matrices as above):
|
degrades Pcodec which handles byte organization internally; Zstd-3 without shuffle numbers shown
|
||||||
|
for an apples-to-apples comparison).
|
||||||
|
|
||||||
| Matrix size | Pcodec | Zstd-3 | Winner |
|
| Matrix size | Pcodec | Zstd-3 (no shuffle) | Winner |
|
||||||
|-------------|--------|--------|--------|
|
|-------------|--------|---------------------|--------|
|
||||||
| 32×32 f32 | 95 µs / **41 MiB/s** | 57 µs / **68 MiB/s** | Zstd-3 (1.66×) |
|
| 32×32 f32 | 95 µs / **41 MiB/s** | 57 µs / **68 MiB/s** | Zstd-3 (1.66×) |
|
||||||
| 128×128 f32 | 528 µs / **118 MiB/s** | 179 µs / **349 MiB/s** | Zstd-3 (2.95×) |
|
| 128×128 f32 | 528 µs / **118 MiB/s** | 179 µs / **349 MiB/s** | Zstd-3 (2.95×) |
|
||||||
| 512×512 f32 | 1.69 ms / **591 MiB/s** | 1.64 ms / **610 MiB/s** | Parity (3% diff) |
|
| 512×512 f32 | 1.69 ms / **591 MiB/s** | 1.64 ms / **610 MiB/s** | Parity (3% diff) |
|
||||||
|
|
||||||
**Interpretation:** Pcodec's distributional analysis adds fixed per-chunk overhead (~400 µs).
|
Pcodec's fixed per-chunk distributional analysis overhead (~400 µs) dominates at 32×32 (4 KB).
|
||||||
For small chunks (32×32 = 4 KB), this overhead dominates and Zstd-3 wins by 2–3×. At large
|
At 512×512 (1 MB) the speeds converge. **Pcodec's advantage is compression ratio, not encode
|
||||||
chunks (512×512 = 1 MB), they converge. **Pcodec's advantage is compression ratio, not
|
speed** — less data on disk means faster reads and lower storage cost. Enable with
|
||||||
encode speed** — it stores less data on disk, improving read throughput and storage
|
`.with_pcodec()` for write-once/read-many workloads (embedding archives, scientific datasets).
|
||||||
efficiency. Enable with `.with_pcodec()` for write-once / read-many embedding archives.
|
|
||||||
|
|
||||||
### Metadata Throughput
|
### Metadata Throughput
|
||||||
|
|
||||||
|
clawhdf5 accumulates all metadata in memory and serializes in one pass. libhdf5 acquires a
|
||||||
|
global file mutex and flushes to disk on every attribute write or group creation.
|
||||||
|
|
||||||
|
**Attributes and datasets** (k = attribute or dataset count):
|
||||||
|
|
||||||
| Workload | k=4 | k=16 | k=64 | k=128 |
|
| Workload | k=4 | k=16 | k=64 | k=128 |
|
||||||
|----------|-----|------|------|-------|
|
|----------|-----|------|------|-------|
|
||||||
| attrs_write (i64) | 8.05 µs / 494 Kop/s | 17.2 µs / 932 Kop/s | 49.2 µs / 1.30 Mop/s | 87.3 µs / 1.47 Mop/s |
|
| **clawhdf5** attrs_write (i64) | 8.05 µs / 494 Kop/s | 17.2 µs / 932 Kop/s | 49.2 µs / 1.30 Mop/s | 87.3 µs / 1.47 Mop/s |
|
||||||
| attrs_read | 1.06 µs / 3.78 Mop/s | 3.64 µs / 4.39 Mop/s | 15.7 µs / 4.08 Mop/s | 31.3 µs / 4.09 Mop/s |
|
| libhdf5 attrs_write | 100 µs / 40 Kop/s | 170 µs / 94 Kop/s | 472 µs / 136 Kop/s | 929 µs / 138 Kop/s |
|
||||||
| string_attrs (write+read) | 5.17 µs / 774 Kop/s | 16.5 µs / 967 Kop/s | 33.6 µs / 951 Kop/s | — |
|
| **Speedup** | **12.4×** | **9.9×** | **9.6×** | **10.6×** |
|
||||||
| groups_create | 12.1 µs / 330 Kop/s | 33.7 µs / 475 Kop/s | 66.7 µs / 480 Kop/s | 121 µs / 528 Kop/s (k=64) |
|
| clawhdf5 attrs_read | 1.06 µs / 3.78 Mop/s | 3.64 µs / 4.39 Mop/s | 15.7 µs / 4.08 Mop/s | 31.3 µs / 4.09 Mop/s |
|
||||||
| groups_traverse | 664 ns / 6.03 Mop/s | 3.55 µs / 4.50 Mop/s | 4.87 µs / 6.58 Mop/s (k=32) | 10.6 µs / 6.04 Mop/s |
|
| clawhdf5 string_attrs (write+read) | 5.17 µs / 774 Kop/s | 16.5 µs / 967 Kop/s | 33.6 µs / 951 Kop/s | — |
|
||||||
| multi_dataset_write | 10.1 µs / 397 Kop/s | 31.5 µs / 508 Kop/s | 104 µs / 614 Kop/s | — |
|
| clawhdf5 multi_dataset_write | 10.1 µs / 397 Kop/s | 31.5 µs / 508 Kop/s | 104 µs / 614 Kop/s | — |
|
||||||
|
|
||||||
|
**Groups** (k = group count):
|
||||||
|
|
||||||
|
| Workload | k=4 | k=16 | k=32 | k=64 |
|
||||||
|
|----------|-----|------|------|------|
|
||||||
|
| **clawhdf5** groups_create | 12.1 µs / 330 Kop/s | 33.7 µs / 475 Kop/s | 66.7 µs / 480 Kop/s | 121 µs / 529 Kop/s |
|
||||||
|
| libhdf5 groups_create | 140 µs / 28 Kop/s | 433 µs / 37 Kop/s | 690 µs / 46 Kop/s | 1,340 µs / 48 Kop/s |
|
||||||
|
| **Speedup** | **11.6×** | **12.8×** | **9.5×** | **11.1×** |
|
||||||
|
| clawhdf5 groups_traverse | 664 ns / 6.0 Mop/s | 3.55 µs / 4.5 Mop/s | 4.87 µs / 6.6 Mop/s | 10.6 µs / 6.0 Mop/s |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## vs libhdf5 Summary
|
||||||
|
|
||||||
|
| Workload | clawhdf5 | libhdf5 | Speedup |
|
||||||
|
|----------|----------|---------|---------|
|
||||||
|
| Sequential read, 1K f32 | 634 ns | 45.2 µs | **71×** |
|
||||||
|
| Sequential read, 100K f32 | 24.5 µs · 15.2 GiB/s | 73.9 µs · 5.0 GiB/s | **3.0×** |
|
||||||
|
| Sequential write, 100K f32 | 228 µs · 1.63 GiB/s | 214 µs · 1.74 GiB/s | **≈ tie** |
|
||||||
|
| Chunked write deflate-6, 512×512 | 1,390 µs · 719 MiB/s | 53,300 µs · 19 MiB/s | **38.4×** |
|
||||||
|
| Attribute write, 128 attrs | 87.3 µs · 1.47 Mop/s | 929 µs · 138 Kop/s | **10.6×** |
|
||||||
|
| Group create, 64 groups | 121 µs · 529 Kop/s | 1,340 µs · 48 Kop/s | **11.1×** |
|
||||||
|
|
||||||
|
### Why the Gaps
|
||||||
|
|
||||||
|
**Metadata (10–13×):** libhdf5 was designed for MPI parallel filesystems where every metadata
|
||||||
|
write must be immediately visible to other processes. It acquires a global file mutex and
|
||||||
|
flushes to disk per operation. clawhdf5 builds the entire file in memory and writes it in one
|
||||||
|
shot — no locking, no flushing, no C heap allocation per message.
|
||||||
|
|
||||||
|
**Chunked compressed write (4–38×):** libhdf5 writes each chunk individually through its VFL
|
||||||
|
(Virtual File Layer), one `pwrite()` per chunk. clawhdf5 compresses all chunks in memory (Rayon
|
||||||
|
parallel when > 2 chunks), lays them out contiguously, and issues a single `write()`. The
|
||||||
|
speedup compounds with matrix size: libhdf5's per-chunk overhead is proportional to chunk count
|
||||||
|
while clawhdf5's architectural cost is constant.
|
||||||
|
|
||||||
|
**Small reads (20–71×):** libhdf5's per-open overhead (chunk cache init, SWMR lock, metadata
|
||||||
|
read) dominates at sub-millisecond payloads. clawhdf5 has no global state — `File::from_bytes()`
|
||||||
|
starts parsing immediately.
|
||||||
|
|
||||||
|
**Large contiguous writes (≈ tie at 100K):** Both are bottlenecked by the OS `write()` syscall
|
||||||
|
to the page cache. There is no algorithmic headroom above ~1.7 GiB/s on this hardware.
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- libhdf5 f64 read comparison excluded — clawhdf5's f32 datatype encoding differs from libhdf5's (known compatibility gap). f64 results are clawhdf5-only.
|
||||||
|
- Serial benchmarks. clawhdf5 uses Rayon for chunk compression when > 2 chunks; that parallelism is already reflected in the chunked write numbers.
|
||||||
|
- clawhdf5 reads from `Vec<u8>` (zero-copy from mmap in production); libhdf5 reads from a temp file. This gives clawhdf5 a structural read advantage that reflects realistic API usage.
|
||||||
|
|||||||
-235
@@ -1,235 +0,0 @@
|
|||||||
# clawhdf5 Benchmark Report
|
|
||||||
|
|
||||||
**Pure-Rust HDF5 vs libhdf5 1.14.6 — head-to-head comparison**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## System
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| CPU | Intel i7-12650H (10C/16T, 4.7 GHz boost) |
|
|
||||||
| RAM | 32 GB DDR5 |
|
|
||||||
| OS | Linux 7.0.0 |
|
|
||||||
| Rust | 1.96.0, `--release` profile |
|
|
||||||
| libhdf5 | 1.14.6 (system install) |
|
|
||||||
| Harness | Criterion 0.5, 100 samples per benchmark |
|
|
||||||
| Date | 2026-06-30 |
|
|
||||||
|
|
||||||
Run command:
|
|
||||||
```bash
|
|
||||||
cargo bench -p clawhdf5-bench --features libhdf5-compare
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sequential Read (f32, in-memory)
|
|
||||||
|
|
||||||
Both implementations read a 1-D contiguous f32 dataset from an in-memory buffer.
|
|
||||||
libhdf5 reads from a temp file on disk; clawhdf5 parses from `Vec<u8>`.
|
|
||||||
|
|
||||||
| Elements | clawhdf5 | libhdf5 | Speedup |
|
|
||||||
|----------|----------|---------|---------|
|
|
||||||
| 1,000 | 563 ns · **6.6 GiB/s** | 45.2 µs · 85 MiB/s | **80×** |
|
|
||||||
| 10,000 | 2.19 µs · **17.0 GiB/s** | 47.8 µs · 799 MiB/s | **22×** |
|
|
||||||
| 100,000 | 25.2 µs · **14.8 GiB/s** | 73.9 µs · **5.0 GiB/s** | **3×** |
|
|
||||||
|
|
||||||
**Key finding:** libhdf5 pays ~45 µs of fixed overhead per open (VFL dispatch, SWMR lock, chunk cache init) regardless of payload size. At 1K elements that overhead is 80× the actual read time. At 100K both are dominated by memory bandwidth and the gap narrows to 3×.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sequential Read (f64, in-memory)
|
|
||||||
|
|
||||||
| Elements | clawhdf5 | Throughput |
|
|
||||||
|----------|----------|-----------|
|
|
||||||
| 1,000 | 708 ns | **10.5 GiB/s** |
|
|
||||||
| 10,000 | 4.43 µs | **16.8 GiB/s** |
|
|
||||||
| 100,000 | 42.5 µs | **17.5 GiB/s** |
|
|
||||||
|
|
||||||
*(libhdf5 comparison not available for f64 — clawhdf5's f32 datatype serialization differs from libhdf5's, so cross-reading was excluded to avoid format-mismatch errors. f64 results are clawhdf5-only.)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Read from Disk (f64, OS I/O included)
|
|
||||||
|
|
||||||
Both implementations write to a temp file, then read it back — measuring `open + read + parse` together.
|
|
||||||
|
|
||||||
| Elements | clawhdf5 | Throughput |
|
|
||||||
|----------|----------|-----------|
|
|
||||||
| 10,000 | 8.9 µs | **8.4 GiB/s** |
|
|
||||||
| 100,000 | 69.3 µs | **10.7 GiB/s** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2-D Chunked Read (f32, from disk)
|
|
||||||
|
|
||||||
clawhdf5 writes a chunked matrix to disk (chunk rows = 32), then reads it back reassembling all chunks.
|
|
||||||
|
|
||||||
| Matrix | clawhdf5 | Throughput |
|
|
||||||
|--------|----------|-----------|
|
|
||||||
| 64 × 64 | 6.39 µs | **2.4 GiB/s** |
|
|
||||||
| 256 × 256 | 47.8 µs | **5.1 GiB/s** |
|
|
||||||
| 512 × 512 | 172.9 µs | **5.6 GiB/s** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sequential Write (f32 contiguous)
|
|
||||||
|
|
||||||
Write N f32 elements as a 1-D contiguous dataset to a file on disk.
|
|
||||||
|
|
||||||
| Elements | clawhdf5 | libhdf5 | Speedup |
|
|
||||||
|----------|----------|---------|---------|
|
|
||||||
| 1,000 | 9.8 µs · **390 MiB/s** | 77.9 µs · 49 MiB/s | **8×** |
|
|
||||||
| 10,000 | 25.0 µs · **1.49 GiB/s** | 87.8 µs · 435 MiB/s | **3.5×** |
|
|
||||||
| 100,000 | 215 µs · **1.73 GiB/s** | 214 µs · 1.74 GiB/s | **≈ parity** |
|
|
||||||
|
|
||||||
**Key finding:** Both converge at 100K where the bottleneck is the `write()` syscall to the OS page cache — there is no algorithmic headroom left. clawhdf5's advantage at smaller sizes is purely startup overhead avoided.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2-D Chunked Write (f32, deflate level 6)
|
|
||||||
|
|
||||||
Write an M×N f32 matrix with chunked layout and deflate compression.
|
|
||||||
|
|
||||||
| Matrix | clawhdf5 | libhdf5 | Speedup |
|
|
||||||
|--------|----------|---------|---------|
|
|
||||||
| 32 × 32 | 20.2 µs · **194 MiB/s** | 172 µs · 23 MiB/s | **8.5×** |
|
|
||||||
| 128 × 128 | 171 µs · **366 MiB/s** | 3.15 ms · 20 MiB/s | **18×** |
|
|
||||||
| 512 × 512 | 3.33 ms · **300 MiB/s** | 53.3 ms · 19 MiB/s | **16×** |
|
|
||||||
|
|
||||||
**Key finding:** This is the largest gap in the entire suite. libhdf5's chunked write path flushes each chunk to the POSIX file individually (53 ms for 1 M elements). clawhdf5 compresses all chunks in-memory and writes the file in a single pass (3.3 ms) — **16× faster**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## f64 Batch Write (embedding workload)
|
|
||||||
|
|
||||||
Write a single f64 1-D dataset of N elements — the hot path for agent memory embedding storage.
|
|
||||||
|
|
||||||
| Elements | clawhdf5 | Throughput |
|
|
||||||
|----------|----------|-----------|
|
|
||||||
| 128 | 6.55 µs | **147 MiB/s** |
|
|
||||||
| 512 | 9.87 µs | **396 MiB/s** |
|
|
||||||
| 1,024 | 11.4 µs | **687 MiB/s** |
|
|
||||||
|
|
||||||
HDF5 header overhead dominates at 128 elements; throughput rises sharply as element count grows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Multi-Dataset Write
|
|
||||||
|
|
||||||
Write K independent f32 datasets into one file (stresses compact → dense link-storage transition at K > 8).
|
|
||||||
|
|
||||||
| Datasets | clawhdf5 | Throughput |
|
|
||||||
|----------|----------|-----------|
|
|
||||||
| 4 | 10.8 µs | 369 Kop/s |
|
|
||||||
| 16 | 31.1 µs | 514 Kop/s |
|
|
||||||
| 64 | 111 µs | 574 Kop/s |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metadata — Attribute Write (i64 scalar)
|
|
||||||
|
|
||||||
Create K attributes on a single dataset. Exercises compact → dense object header transition.
|
|
||||||
|
|
||||||
| Attributes | clawhdf5 | libhdf5 | Speedup |
|
|
||||||
|------------|----------|---------|---------|
|
|
||||||
| 4 | 7.9 µs · 502 Kop/s | 100 µs · 40 Kop/s | **12.6×** |
|
|
||||||
| 16 | 16.1 µs · 994 Kop/s | 170 µs · 94 Kop/s | **10.6×** |
|
|
||||||
| 64 | 46.9 µs · 1.36 Mop/s | 472 µs · 136 Kop/s | **10×** |
|
|
||||||
| 128 | 87.8 µs · 1.46 Mop/s | 929 µs · 138 Kop/s | **10.6×** |
|
|
||||||
|
|
||||||
**Key finding:** Consistently **10–13× faster** at all scales. libhdf5 acquires a global file mutex and flushes the object header to disk on every attribute write. clawhdf5 accumulates all attributes in-memory and serializes in one pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metadata — Attribute Read
|
|
||||||
|
|
||||||
Open a pre-built in-memory file and read all K attributes back.
|
|
||||||
|
|
||||||
| Attributes | clawhdf5 | Throughput |
|
|
||||||
|------------|----------|-----------|
|
|
||||||
| 4 | 982 ns | **4.07 Mop/s** |
|
|
||||||
| 16 | 3.68 µs | **4.35 Mop/s** |
|
|
||||||
| 64 | 13.3 µs | **4.80 Mop/s** |
|
|
||||||
| 128 | 27.5 µs | **4.65 Mop/s** |
|
|
||||||
|
|
||||||
Attribute read throughput is flat above 4 Mop/s across all scales — the compact→dense transition at K=8 is transparent to the reader.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metadata — Group Create
|
|
||||||
|
|
||||||
Create K top-level groups, each containing one dataset.
|
|
||||||
|
|
||||||
| Groups | clawhdf5 | libhdf5 | Speedup |
|
|
||||||
|--------|----------|---------|---------|
|
|
||||||
| 4 | 11.8 µs · 340 Kop/s | 140 µs · 28 Kop/s | **11.9×** |
|
|
||||||
| 16 | 37.3 µs · 429 Kop/s | 433 µs · 37 Kop/s | **11.6×** |
|
|
||||||
| 32 | 72.3 µs · 443 Kop/s | 690 µs · 46 Kop/s | **9.5×** |
|
|
||||||
| 64 | 121 µs · 527 Kop/s | 1.34 ms · 48 Kop/s | **11.1×** |
|
|
||||||
|
|
||||||
**Key finding:** **10–12× faster** at every scale. Same reason as attributes — libhdf5 fsync-like behavior per group creation vs clawhdf5's single-pass write.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metadata — Group Traversal
|
|
||||||
|
|
||||||
Open a pre-built in-memory file and list the root group's children.
|
|
||||||
|
|
||||||
| Groups | clawhdf5 | Throughput |
|
|
||||||
|--------|----------|-----------|
|
|
||||||
| 4 | 614 ns | **6.51 Mop/s** |
|
|
||||||
| 16 | 2.36 µs | **6.77 Mop/s** |
|
|
||||||
| 32 | 5.25 µs | **6.10 Mop/s** |
|
|
||||||
| 64 | 9.48 µs | **6.75 Mop/s** |
|
|
||||||
|
|
||||||
Group traversal scales linearly and stays above 6 Mop/s — listing 64 groups costs under 10 µs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metadata — String Attribute Round-trip
|
|
||||||
|
|
||||||
Write K variable-length string attributes then immediately read them back (write + read in one benchmark iteration).
|
|
||||||
|
|
||||||
| Attributes | clawhdf5 | Throughput |
|
|
||||||
|------------|----------|-----------|
|
|
||||||
| 4 | 4.83 µs | 829 Kop/s |
|
|
||||||
| 16 | 17.9 µs | 896 Kop/s |
|
|
||||||
| 32 | 29.7 µs | **1.08 Mop/s** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary Table
|
|
||||||
|
|
||||||
| Workload | clawhdf5 | libhdf5 | Winner |
|
|
||||||
|----------|----------|---------|--------|
|
|
||||||
| Sequential read, small (1K f32) | 563 ns | 45.2 µs | **clawhdf5 80×** |
|
|
||||||
| Sequential read, large (100K f32) | 25 µs · 14.8 GiB/s | 74 µs · 5.0 GiB/s | **clawhdf5 3×** |
|
|
||||||
| Sequential write, large (100K f32) | 215 µs · 1.73 GiB/s | 214 µs · 1.74 GiB/s | **tie** |
|
|
||||||
| Chunked write + deflate (512×512) | 3.33 ms · 300 MiB/s | 53.3 ms · 19 MiB/s | **clawhdf5 16×** |
|
|
||||||
| Attribute write (128 attrs) | 87.8 µs · 1.46 Mop/s | 929 µs · 138 Kop/s | **clawhdf5 10.6×** |
|
|
||||||
| Attribute read (128 attrs) | 27.5 µs · 4.65 Mop/s | — | — |
|
|
||||||
| Group create (64 groups) | 121 µs · 527 Kop/s | 1.34 ms · 48 Kop/s | **clawhdf5 11×** |
|
|
||||||
| Group traversal (64 groups) | 9.48 µs · 6.75 Mop/s | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Interpretation
|
|
||||||
|
|
||||||
### Where clawhdf5 wins by a large margin
|
|
||||||
|
|
||||||
- **All metadata operations (10–13×):** libhdf5 was designed for MPI parallel filesystems where every metadata write must be immediately visible to other processes. It acquires a global file mutex and flushes to disk per operation. clawhdf5 builds the entire file in memory and writes it in one shot — no locking, no flushing, no C heap allocation per message.
|
|
||||||
|
|
||||||
- **Chunked compressed write (16–18×):** libhdf5 writes each chunk individually through its VFL (Virtual File Layer), which means one `pwrite()` call per chunk. clawhdf5 compresses all chunks in parallel (Rayon), lays them out in memory, and issues a single `write()`.
|
|
||||||
|
|
||||||
- **Small reads (22–80×):** libhdf5's per-open overhead (cache init, lock acquisition, metadata read) dominates at sub-millisecond payloads. clawhdf5 has no global state — `File::from_bytes()` starts parsing immediately.
|
|
||||||
|
|
||||||
### Where they are equal
|
|
||||||
|
|
||||||
- **Large contiguous writes at 100K+ elements:** Both are bottlenecked on the OS `write()` syscall to the page cache. The underlying deflate and memcpy throughput is identical at this scale.
|
|
||||||
|
|
||||||
### Caveats
|
|
||||||
|
|
||||||
- **No libhdf5 f64 read comparison:** clawhdf5's f32 datatype message uses a different on-disk encoding than libhdf5 expects (known compatibility gap being tracked). f64 comparisons were excluded to avoid false failures.
|
|
||||||
- **Single-threaded:** These are serial benchmarks. clawhdf5 uses Rayon for chunk compression internally; that parallelism is already reflected in the numbers above.
|
|
||||||
- **In-memory reads:** clawhdf5's read path operates on a `Vec<u8>` (zero-copy from mmap in production); libhdf5 reads from a temp file. This gives clawhdf5 a structural advantage on the read side that reflects realistic usage of the two APIs.
|
|
||||||
Reference in New Issue
Block a user