- 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]>
291 lines
11 KiB
Rust
291 lines
11 KiB
Rust
//! h5bench-equivalent read workloads for clawhdf5.
|
|
//!
|
|
//! Covers sequential read, hyperslab / strided access, and round-trip
|
|
//! validation patterns mirroring the h5bench HPC read suite.
|
|
|
|
use clawhdf5::{File, FileBuilder};
|
|
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
|
use tempfile::TempDir;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers: build reference files once per bench group.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Write a contiguous 1-D f32 dataset and return raw bytes.
|
|
fn make_1d_contiguous_bytes(n: usize) -> Vec<u8> {
|
|
let data: Vec<f32> = (0..n).map(|i| i as f32 * 0.001).collect();
|
|
let mut fb = FileBuilder::new();
|
|
fb.create_dataset("data")
|
|
.with_f32_data(&data)
|
|
.with_shape(&[n as u64]);
|
|
fb.finish().unwrap()
|
|
}
|
|
|
|
/// Write a contiguous 1-D f64 dataset and return raw bytes.
|
|
fn make_1d_f64_bytes(n: usize) -> Vec<u8> {
|
|
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.finish().unwrap()
|
|
}
|
|
|
|
/// Write a 2-D chunked f32 matrix to a temp file, return path string.
|
|
///
|
|
/// The temp dir is returned to keep the directory alive.
|
|
fn make_2d_chunked_file(tmp: &TempDir, rows: usize, cols: usize) -> std::path::PathBuf {
|
|
let data: Vec<f32> = (0..rows * cols).map(|i| i as f32).collect();
|
|
let path = tmp.path().join("chunked.h5");
|
|
let mut fb = FileBuilder::new();
|
|
fb.create_dataset("matrix")
|
|
.with_f32_data(&data)
|
|
.with_shape(&[rows as u64, cols as u64])
|
|
.with_chunks(&[32, cols as u64]);
|
|
fb.write(&path).unwrap();
|
|
path
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workload: read_sequential
|
|
// Read back the full 1-D contiguous f32 dataset.
|
|
// Measures parser + byte-copy throughput.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn bench_read_sequential(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read_sequential");
|
|
|
|
for &n in &[1_000usize, 10_000, 100_000] {
|
|
let bytes = make_1d_contiguous_bytes(n);
|
|
group.throughput(Throughput::Bytes((n * size_of::<f32>()) as u64));
|
|
|
|
group.bench_with_input(BenchmarkId::new("clawhdf5", n), &bytes, |b, raw| {
|
|
b.iter(|| {
|
|
let file = File::from_bytes(raw.clone()).unwrap();
|
|
let ds = file.dataset("data").unwrap();
|
|
ds.read_f32().unwrap()
|
|
});
|
|
});
|
|
|
|
#[cfg(feature = "libhdf5-compare")]
|
|
group.bench_with_input(BenchmarkId::new("libhdf5", n), &n, |b, &nn| {
|
|
let tmp = TempDir::new().unwrap();
|
|
let path = tmp.path().join("seq_libhdf5.h5");
|
|
let data: Vec<f32> = (0..nn).map(|i| i as f32 * 0.001).collect();
|
|
{
|
|
let lf = hdf5::File::create(&path).unwrap();
|
|
let lds = lf
|
|
.new_dataset::<f32>()
|
|
.shape([nn])
|
|
.create("data")
|
|
.unwrap();
|
|
lds.write(data.as_slice()).unwrap();
|
|
}
|
|
b.iter(|| {
|
|
let file = hdf5::File::open(&path).unwrap();
|
|
let ds = file.dataset("data").unwrap();
|
|
ds.read_raw::<f32>().unwrap()
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workload: read_f64_sequential
|
|
// Same as above but for f64 — the dominant agent-embedding dtype.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn bench_read_f64_sequential(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read_f64_sequential");
|
|
|
|
for &n in &[1_000usize, 10_000, 100_000] {
|
|
let bytes = make_1d_f64_bytes(n);
|
|
group.throughput(Throughput::Bytes((n * size_of::<f64>()) as u64));
|
|
|
|
group.bench_with_input(BenchmarkId::new("clawhdf5", n), &bytes, |b, raw| {
|
|
b.iter(|| {
|
|
let file = File::from_bytes(raw.clone()).unwrap();
|
|
let ds = file.dataset("data").unwrap();
|
|
ds.read_f64().unwrap()
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workload: read_chunked_2d
|
|
// Read back a 2-D chunked f32 matrix from disk (exercises chunk reassembly).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn bench_read_chunked_2d(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read_chunked_2d");
|
|
|
|
for &(rows, cols) in &[(64usize, 64usize), (256, 256), (512, 512)] {
|
|
let tmp = TempDir::new().unwrap();
|
|
let path = make_2d_chunked_file(&tmp, rows, cols);
|
|
let n = rows * cols;
|
|
group.throughput(Throughput::Bytes((n * size_of::<f32>()) as u64));
|
|
let label = format!("{rows}x{cols}");
|
|
|
|
group.bench_with_input(BenchmarkId::new("clawhdf5", &label), &path, |b, p| {
|
|
b.iter(|| {
|
|
let raw = std::fs::read(p).unwrap();
|
|
let file = File::from_bytes(raw).unwrap();
|
|
let ds = file.dataset("matrix").unwrap();
|
|
ds.read_f32().unwrap()
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workload: read_from_disk
|
|
// Open file from disk (FileBuilder::write → File::open) measuring OS I/O +
|
|
// HDF5 parse together. Simulates cold-cache reads.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn bench_read_from_disk(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read_from_disk");
|
|
|
|
for &n in &[10_000usize, 100_000] {
|
|
let tmp = TempDir::new().unwrap();
|
|
let path = tmp.path().join("disk.h5");
|
|
|
|
let data: Vec<f64> = (0..n).map(|i| i as f64).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", 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()
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workload: read_hyperslab
|
|
// Reads a subset of a 1-D dataset (simulating strided / hyperslab access).
|
|
// Uses every-other element to stress the selection logic.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn bench_read_hyperslab(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read_hyperslab");
|
|
|
|
for &n in &[10_000usize, 100_000] {
|
|
let bytes = make_1d_f64_bytes(n);
|
|
// Read first 10% of the dataset as a proxy for hyperslab access.
|
|
let slice_len = n / 10;
|
|
group.throughput(Throughput::Bytes((slice_len * size_of::<f64>()) as u64));
|
|
|
|
group.bench_with_input(BenchmarkId::new("clawhdf5", n), &bytes, |b, raw| {
|
|
b.iter(|| {
|
|
let file = File::from_bytes(raw.clone()).unwrap();
|
|
let ds = file.dataset("data").unwrap();
|
|
// Full read then take a slice — clawhdf5 does not yet expose
|
|
// selection API at the high-level facade, so we read all and
|
|
// trim (this is what the format-level selection exercises).
|
|
let all = ds.read_f64().unwrap();
|
|
all[..slice_len].to_vec()
|
|
});
|
|
});
|
|
}
|
|
|
|
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!(
|
|
read_benches,
|
|
bench_read_sequential,
|
|
bench_read_f64_sequential,
|
|
bench_read_chunked_2d,
|
|
bench_read_from_disk,
|
|
bench_read_hyperslab,
|
|
bench_read_zerocopy_mmap,
|
|
);
|
|
criterion_main!(read_benches);
|