docs: fix untraceable benchmark claims, add dual-audience framing, validate on second machine

- README's "HDF5 Core I/O" table claimed 19ns/2,080µs labeled 308× (real ratio
  ~109,000×) and a 313ns zero-copy mmap figure — neither traced to any dated
  benchmark in BENCHMARKS.md. Replaced the table wholesale with the existing
  "vs libhdf5 Summary" figures, relabeled from "h5py/C HDF5" to "libhdf5"
  (BENCHMARKS.md never benchmarks against h5py, only libhdf5 directly).
- Added two new Criterion benchmarks to close the coverage gaps that produced
  the untraceable numbers: metadata_open_from_disk (I/O-inclusive, fair
  clawhdf5-vs-libhdf5 file-open comparison) and metadata_parse_in_memory
  (clawhdf5-only, explicitly labeled as excluding I/O) in h5bench_meta.rs;
  read_zerocopy_mmap in h5bench_read.rs (forces real page-ins by summing
  elements rather than just returning a slice length — the mmap path turns
  out to be slower than a plain copy at these sizes, an honest, unflattering
  but real result now documented instead of a fabricated 313ns).
- Re-ran the full existing benchmark suite plus the two new ones on a second,
  independently administered machine (tank: Ryzen 7 7800X3D) to validate the
  numbers before publishing them. 5 of 6 rows landed within ~15% of the
  original i7-12650H figures; recorded both in BENCHMARKS.md's new
  "Independent Validation" section. README now cites the tank numbers.
- Added a short top-of-file README callout naming both halves of the project
  (general-purpose HDF5 library vs. agent memory layer) with links to
  BENCHMARKS.md and the Crate Map, so a data-infra reader isn't 60% through
  a memory-store pitch before finding the part relevant to them.
- Added one factual, no-names line noting benchmark numbers are being
  validated in collaboration with HDF5 Group engineers.
- Fixed the same untraceable "2-300x faster than h5py/C HDF5" / "313 ns"
  claims in docs/QUICKSTART.md, one click from the README's own "New here?"
  link.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-03 17:46:55 -07:00
co-authored by Claude Sonnet 5
parent 6b1ea450f5
commit 88195d1c33
5 changed files with 278 additions and 13 deletions
+85
View File
@@ -564,3 +564,88 @@ to the page cache. There is no algorithmic headroom above ~1.7 GiB/s on this har
- libhdf5 f64 read comparison excluded — clawhdf5's f32 datatype encoding differs from libhdf5's (known compatibility gap). f64 results are clawhdf5-only. - 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. - 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. - 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.
---
## Independent Validation: tank (Ryzen 7 7800X3D), 2026-08-03
The `vs libhdf5 Summary` numbers above were re-run on a second, independently
administered machine (`tank`: AMD Ryzen 7 7800X3D, 8C/16T, Ubuntu 26.04, libhdf5
1.14.6 via `apt`) to confirm they reproduce off the original i7-12650H box, and to
add benchmark coverage for two claims that a documentation review found were not
traceable to any dated benchmark run (see git history around 2026-08-03 for context).
This section documents both.
### Reproduction of the vs-libhdf5 Summary table
| Workload | clawhdf5 (tank) | libhdf5 (tank) | Speedup (tank) | Speedup (i7-12650H, above) |
|----------|-----------------|-----------------|----------------|------------------------------|
| Sequential read, 1K f32 | 553 ns | 44.2 µs | **79.9×** | 71× |
| Sequential read, 100K f32 | 23.3 µs | 63.6 µs | **2.7×** | 3.0× |
| Sequential write, 100K f32 | 210 µs | 189 µs | **≈ tie** (clawhdf5 ~11% behind) | ≈ tie (clawhdf5 ~7% behind) |
| Chunked write deflate-6, 512×512 | 1.44 ms | 65.0 ms | **45.3×** | 38.4× |
| Attribute write, 128 attrs | 85.2 µs | 877 µs | **10.3×** | 10.6× |
| Group create, 64 groups | 130 µs | 1.37 ms | **10.6×** | 11.1× |
Five of six rows land within ~15% of the original i7-12650H figures — consistent
with normal cross-machine variance, not a methodology artifact. The chunked-write
row moved further (38.4× → 45.3×, +18%): tank's libhdf5 per-chunk write cost scales
worse relative to its own sequential-write throughput than on the i7, likely IPC/
memory-subsystem dependent. Both figures are real and dated; we report both rather
than picking one.
### New coverage: replacing the retracted "metadata parse / 308×" and "zero-copy mmap / 313 ns" claims
An earlier README revision cited `19 ns` vs `2,080 µs` (labeled, incorrectly, `308×`)
for "metadata parse," and `313 ns` for "zero-copy mmap" — neither figure traced to
any benchmark in this file. Both have been retracted from the README. In their
place, two new Criterion benchmarks were added
(`crates/clawhdf5-bench/benches/h5bench_meta.rs`,
`crates/clawhdf5-bench/benches/h5bench_read.rs`) and run on tank:
**`metadata_open_from_disk`** — opens a small file from disk (`std::fs::read` /
`hdf5::File::open`) and resolves one attribute. Both sides pay real OS I/O, unlike
the retracted claim.
| Operation | clawhdf5 | libhdf5 | Speedup |
|-----------|----------|---------|---------|
| Open file + read 1 attribute | 4.01 µs | 39.3 µs | **9.8×** |
**`metadata_parse_in_memory`** (clawhdf5-only) — times `File::from_bytes()` alone,
given bytes already resident in memory, i.e. header-parse cost with disk I/O
excluded. There is no fair libhdf5-side equivalent (its API has no "parse from an
in-memory buffer, skip the OS open" path), so this is reported standalone rather
than as a speedup multiple — this is the honest version of what the old `19 ns`
number was trying to claim.
| Operation | clawhdf5 (in-memory, no I/O) |
|-----------|------------------------------|
| Parse superblock + resolve 1 attribute | 549 ns |
**`read_zerocopy_mmap`** — opens via `MmapFile` and reads an f64 dataset through
`read_f64_zerocopy()`, summing every element to force the mapped pages to actually
fault in (returning only a slice length, as an earlier draft of this benchmark did,
would repeat the exact "measures nothing" mistake being fixed here).
| n (f64 elements) | clawhdf5 mmap (zerocopy, page-fault-forced) | clawhdf5 (`Vec<u8>` copy) | libhdf5 (disk open + copy) |
|-------------------|----------------------------------------------|----------------------------|------------------------------|
| 1,000 | 7.86 µs | 4.50 µs | 44.2 µs |
| 10,000 | 19.0 µs | 9.53 µs | 47.1 µs |
| 100,000 | 112 µs | 72.0 µs | 81.2 µs |
Honest result: at these sizes, forcing full materialization through the mmap path
is **not** faster than the plain `Vec<u8>` copy path — `mmap()`/page-fault overhead
per call outweighs the copy it avoids. This contradicts the retracted `313 ns`
claim outright and is a genuinely useful finding: `MmapFile`'s real advantage is
avoiding the allocation/copy for large files or sparse access patterns (lower peak
RSS, share pages across processes), not raw single-shot read latency at these
sizes. No README claim is made from this row; it's recorded here for the record
and to keep future readers from reintroducing the old number.
**Reproduce:**
```bash
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_meta -- metadata_open_from_disk
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_meta -- metadata_parse_in_memory
cargo bench -p clawhdf5-bench --features libhdf5-compare --bench h5bench_read -- read_zerocopy_mmap
```
+21 -11
View File
@@ -10,8 +10,13 @@
ClawHDF5 is a pure-Rust HDF5 implementation combined with a research-grade agent memory engine. It gives AI agents persistent, searchable, cryptographically verifiable memory — all stored in a single portable file. ClawHDF5 is a pure-Rust HDF5 implementation combined with a research-grade agent memory engine. It gives AI agents persistent, searchable, cryptographically verifiable memory — all stored in a single portable file.
> **Two things live here:**
> - **A general-purpose, pure-Rust HDF5 library** — zero C dependencies, NetCDF-4 support, SIMD/GPU acceleration. See the **[Crate Map](#crate-map)** and **[BENCHMARKS.md](BENCHMARKS.md)** for the libhdf5 head-to-head numbers.
> - **An agent memory layer built on top of it** — vector search, knowledge graph, hippocampal-style consolidation, in `clawhdf5-agent`.
``` ```
cargo add clawhdf5-agent --features agent cargo add clawhdf5 # core HDF5 read/write, no agent layer
cargo add clawhdf5-agent --features agent # + agent memory layer
``` ```
> **New here?** Start with the **[Quickstart Guide](docs/QUICKSTART.md)** · See **[Use Cases](docs/USE_CASES.md)** · Read **[Benchmarks](BENCHMARKS.md)** > **New here?** Start with the **[Quickstart Guide](docs/QUICKSTART.md)** · See **[Use Cases](docs/USE_CASES.md)** · Read **[Benchmarks](BENCHMARKS.md)**
@@ -37,7 +42,21 @@ Every AI agent needs memory. Today that means scattered Markdown files, SQLite d
## Performance ## Performance
Benchmarked on Intel i7-12650H (10C/16T), 384-dim embeddings, Criterion.rs. Vector search and agent-memory operations below are benchmarked on Intel i7-12650H (10C/16T), 384-dim embeddings, Criterion.rs. The HDF5 Core I/O table immediately below is from a separate, independently reproduced run (see its own hardware note).
### HDF5 Core I/O (vs libhdf5 1.14.6)
*Benchmark numbers are being validated in collaboration with engineers from the HDF5 Group to confirm methodology and reproducibility.*
Figures below are from an independent reproduction run on a second machine (AMD Ryzen 7 7800X3D, 2026-08-03). Full methodology, the original i7-12650H run, and two additional benchmarks added to close prior coverage gaps (an I/O-inclusive metadata-open comparison and an honest zero-copy-mmap measurement) are in [BENCHMARKS.md § Independent Validation](BENCHMARKS.md#independent-validation-tank-ryzen-7-7800x3d-2026-08-03).
| Operation | ClawhDF5 | libhdf5 | Speedup |
|-----------|----------|---------|---------|
| Attribute write (128 attrs) | 85.2 µs | 877 µs | **10.3×** |
| Group create (64 groups) | 130 µs | 1.37 ms | **10.6×** |
| Chunked write, deflate-6 (512×512 f32) | 1.44 ms | 65.0 ms | **45.3×** |
| Sequential read (100K f32) | 23.3 µs | 63.6 µs | **2.7×** |
| Sequential write (100K f32) | 210 µs | 189 µs | **≈ tie** |
### Vector Search ### Vector Search
@@ -60,15 +79,6 @@ Benchmarked on Intel i7-12650H (10C/16T), 384-dim embeddings, Criterion.rs.
| Memory write (WAL) | **18 µs** | per record (group-commit append; HDF5 batched at flush) | | Memory write (WAL) | **18 µs** | per record (group-commit append; HDF5 batched at flush) |
| Importance gate | **61 ns** | per record | | Importance gate | **61 ns** | per record |
### HDF5 Core I/O (vs h5py/C HDF5)
| Operation | ClawhDF5 | h5py (C) | Speedup |
|-----------|----------|----------|---------|
| Metadata parse | 19 ns | 2,080 µs | **308×** |
| Write 1M f64 | 0.82 ms | 1.60 ms | **2×** |
| Read 1M f64 | 0.28 ms | 0.65 ms | **2.3×** |
| Zero-copy mmap | 313 ns | N/A | — |
### Chunked Write Throughput (codec comparison) ### Chunked Write Throughput (codec comparison)
Measured with Criterion on f32 matrices. Auto-shuffle is applied before all compression codecs Measured with Criterion on f32 matrices. Auto-shuffle is applied before all compression codecs
@@ -220,6 +220,104 @@ fn bench_metadata_string_attrs(c: &mut Criterion) {
group.finish(); group.finish();
} }
// ---------------------------------------------------------------------------
// Workload: metadata_open_from_disk
// Open a small pre-built file from disk and resolve one attribute. Both
// sides pay the OS open()/read() cost plus header-parse cost, so this is a
// fair, I/O-inclusive "open a file and touch its metadata" comparison — the
// honest version of the "metadata parse" claim this benchmark replaces.
// ---------------------------------------------------------------------------
fn bench_metadata_open_from_disk(c: &mut Criterion) {
let mut group = c.benchmark_group("metadata_open_from_disk");
group.throughput(Throughput::Elements(1));
let tmp = TempDir::new().unwrap();
let clawhdf5_path = tmp.path().join("open_clawhdf5.h5");
{
let mut fb = FileBuilder::new();
let ds = fb
.create_dataset("data")
.with_f64_data(&[1.0, 2.0, 3.0])
.with_shape(&[3]);
ds.set_attr("label", AttrValue::I64(42));
fb.write(&clawhdf5_path).unwrap();
}
group.bench_function("clawhdf5", |b| {
b.iter(|| {
let raw = std::fs::read(&clawhdf5_path).unwrap();
let file = File::from_bytes(raw).unwrap();
let ds = file.dataset("data").unwrap();
ds.attrs().unwrap()
});
});
#[cfg(feature = "libhdf5-compare")]
{
let libhdf5_path = tmp.path().join("open_libhdf5.h5");
{
let file = hdf5::File::create(&libhdf5_path).unwrap();
let ds = file
.new_dataset::<f64>()
.shape([3])
.create("data")
.unwrap();
ds.write(&[1.0f64, 2.0, 3.0]).unwrap();
ds.new_attr::<i64>()
.create("label")
.unwrap()
.write_scalar(&42i64)
.unwrap();
}
group.bench_function("libhdf5", |b| {
b.iter(|| {
let file = hdf5::File::open(&libhdf5_path).unwrap();
let ds = file.dataset("data").unwrap();
let _: i64 = ds.attr("label").unwrap().read_scalar().unwrap();
});
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Workload: metadata_parse_in_memory (clawhdf5-only)
// Times File::from_bytes() alone on bytes already resident in memory — i.e.
// the header-parse cost with disk I/O excluded. There is no fair libhdf5
// equivalent (its API has no "parse from an in-memory buffer" path that
// skips the OS open), so this is reported standalone, not as a speedup
// multiple against libhdf5. See metadata_open_from_disk above for the
// I/O-inclusive, directly comparable number.
// ---------------------------------------------------------------------------
fn bench_metadata_parse_in_memory(c: &mut Criterion) {
let mut group = c.benchmark_group("metadata_parse_in_memory");
group.throughput(Throughput::Elements(1));
let bytes = {
let mut fb = FileBuilder::new();
let ds = fb
.create_dataset("data")
.with_f64_data(&[1.0, 2.0, 3.0])
.with_shape(&[3]);
ds.set_attr("label", AttrValue::I64(42));
fb.finish().unwrap()
};
group.bench_with_input(BenchmarkId::new("clawhdf5", "in_memory"), &bytes, |b, raw| {
b.iter(|| {
let file = File::from_bytes(raw.clone()).unwrap();
let ds = file.dataset("data").unwrap();
ds.attrs().unwrap()
});
});
group.finish();
}
criterion_group!( criterion_group!(
meta_benches, meta_benches,
bench_metadata_attrs_write, bench_metadata_attrs_write,
@@ -227,5 +325,7 @@ criterion_group!(
bench_metadata_groups_create, bench_metadata_groups_create,
bench_metadata_groups_traverse, bench_metadata_groups_traverse,
bench_metadata_string_attrs, bench_metadata_string_attrs,
bench_metadata_open_from_disk,
bench_metadata_parse_in_memory,
); );
criterion_main!(meta_benches); criterion_main!(meta_benches);
@@ -209,6 +209,75 @@ fn bench_read_hyperslab(c: &mut Criterion) {
group.finish(); group.finish();
} }
// ---------------------------------------------------------------------------
// Workload: read_zerocopy_mmap
// Opens a file from disk via `MmapFile` and reads an f64 dataset through
// `read_f64_zerocopy()`, which returns a slice directly into the mapped
// pages (no allocation, no copy). Compared against the regular
// std::fs::read + File::from_bytes path (which does copy), and — with
// libhdf5-compare — against libhdf5's own disk-backed open+read.
// ---------------------------------------------------------------------------
fn bench_read_zerocopy_mmap(c: &mut Criterion) {
use clawhdf5::MmapFile;
let mut group = c.benchmark_group("read_zerocopy_mmap");
for &n in &[1_000usize, 10_000, 100_000] {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("mmap.h5");
let data: Vec<f64> = (0..n).map(|i| i as f64 * 0.001).collect();
let mut fb = FileBuilder::new();
fb.create_dataset("data")
.with_f64_data(&data)
.with_shape(&[n as u64]);
fb.write(&path).unwrap();
group.throughput(Throughput::Bytes((n * size_of::<f64>()) as u64));
group.bench_with_input(BenchmarkId::new("clawhdf5_mmap_zerocopy", n), &path, |b, p| {
b.iter(|| {
let file = MmapFile::open(p).unwrap();
let ds = file.dataset("data").unwrap();
let slice = ds.read_f64_zerocopy().unwrap();
// Sum every element to force the mapped pages to actually be
// faulted in — returning just `.len()` would measure nothing
// but the mmap() syscall, repeating the exact "too-fast-to-
// be-real" mistake this benchmark exists to fix.
let sum: f64 = slice.map(|s| s.iter().sum()).unwrap_or(0.0);
criterion::black_box(sum)
});
});
group.bench_with_input(BenchmarkId::new("clawhdf5_copy", n), &path, |b, p| {
b.iter(|| {
let raw = std::fs::read(p).unwrap();
let file = File::from_bytes(raw).unwrap();
file.dataset("data").unwrap().read_f64().unwrap()
});
});
#[cfg(feature = "libhdf5-compare")]
group.bench_with_input(BenchmarkId::new("libhdf5", n), &n, |b, &nn| {
let tmp2 = TempDir::new().unwrap();
let path2 = tmp2.path().join("mmap_libhdf5.h5");
let data2: Vec<f64> = (0..nn).map(|i| i as f64 * 0.001).collect();
{
let lf = hdf5::File::create(&path2).unwrap();
let lds = lf.new_dataset::<f64>().shape([nn]).create("data").unwrap();
lds.write(data2.as_slice()).unwrap();
}
b.iter(|| {
let file = hdf5::File::open(&path2).unwrap();
let ds = file.dataset("data").unwrap();
ds.read_raw::<f64>().unwrap()
});
});
}
group.finish();
}
criterion_group!( criterion_group!(
read_benches, read_benches,
bench_read_sequential, bench_read_sequential,
@@ -216,5 +285,6 @@ criterion_group!(
bench_read_chunked_2d, bench_read_chunked_2d,
bench_read_from_disk, bench_read_from_disk,
bench_read_hyperslab, bench_read_hyperslab,
bench_read_zerocopy_mmap,
); );
criterion_main!(read_benches); criterion_main!(read_benches);
+2 -2
View File
@@ -344,7 +344,7 @@ let data = temp.read_f64()?;
### Performance ### Performance
ClawhDF5 is 2–300× faster than h5py/C HDF5 for common operations. See [BENCHMARKS.md](../BENCHMARKS.md) for details. The zero-copy mmap path reads 1M floats in 313 nanoseconds. ClawhDF5 is 3–45× faster than libhdf5 for common operations (see [BENCHMARKS.md](../BENCHMARKS.md#vs-libhdf5-summary) for methodology and an independent second-machine reproduction).
--- ---
@@ -548,7 +548,7 @@ let final_results = confidence::reject_low_confidence(
- Hierarchical groups (natural fit for entity/relation/session organization) - Hierarchical groups (natural fit for entity/relation/session organization)
- Compression built in (zlib, lz4, zstd) - Compression built in (zlib, lz4, zstd)
- Battle-tested format (30+ years in scientific computing) - Battle-tested format (30+ years in scientific computing)
- Our implementation is pure Rust, 2–300× faster than C HDF5 for metadata ops - Our implementation is pure Rust, 10–11× faster than libhdf5 for metadata ops (attribute writes, group creation) — see [BENCHMARKS.md](../BENCHMARKS.md#vs-libhdf5-summary)
--- ---