Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.5.1

### Changed

- Under the `mmap` feature, the buffer-reuse read paths —
`H5Dataset::read_raw_into` and `read_slice_into`, and the reader's
`read_dataset_raw_into` / `read_slice_into` — serve contiguous reads
of any size from the file's memory map. The map's 8 KiB read ceiling
prices the cold case, where faulting a fresh allocation in during the
copy costs more than `pread` past that size; a caller who keeps the
destination buffer has already paid that fault, and for a kept buffer
the map wins at every size, so the into-reads take it with no ceiling.
Rereading a 128 MiB contiguous dataset into a kept buffer goes from
1.04x libhdf5 to 0.39x, and covering it in 128 KiB slices from 0.92x
to 0.48x. The allocating reads (`read_raw`, `read_slice`) keep the
ceiling: their destination is fresh by construction.

- The same for an unfiltered chunked dataset's into-reads: the chunk
runs that land straight in the caller's buffer (never materializing a
chunk image) carry the same destination fact, so a kept buffer serves
them from the map at any size too. A full 128 MiB chunked reread into
a kept buffer goes from 0.98x libhdf5 to 0.62x, and 1000 random
64 KiB slices from 0.98x to 0.50x. Whole-chunk reads (a fresh image
by construction, and every filtered chunk) are unchanged.

## 0.5.0

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "rust-hdf5"
description = "Pure Rust HDF5 library with full read/write and SWMR support"
version = "0.5.0"
version = "0.5.1"
edition = "2021"
rust-version = "1.89"
license = "MIT"
Expand Down
99 changes: 99 additions & 0 deletions perf/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,55 @@ int main(int argc, char **argv) {
write_contig(path, data, CONTIG_N);
free(data);
TIMED({ sink = view_and_sum(path, CONTIG_N); });
} else if (strcmp(wl, "into-read") == 0) {
/* Buffer reuse: open and the first read are setup, so every timed
* read lands in an already-faulted buffer. */
double *data = ramp(CONTIG_N);
snprintf(path, sizeof path, "%s/c-intoread-in.h5", workdir);
write_contig(path, data, CONTIG_N);
free(data);
double *buf = malloc(CONTIG_N * sizeof(double));
hid_t f = H5Fopen(path, H5F_ACC_RDONLY, H5P_DEFAULT);
hid_t d = H5Dopen2(f, "data", H5P_DEFAULT);
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf));
TIMED({
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf));
if (buf[CONTIG_N - 1] != (double)(CONTIG_N - 1)) abort();
});
H5Dclose(d);
H5Fclose(f);
free(buf);
} else if (strcmp(wl, "into-slice") == 0) {
/* The same reuse per piece: sequential 128 KiB slices into one
* buffer, covering the dataset once per rep. */
const int PIECE = 16 * 1024;
double *data = ramp(CONTIG_N);
snprintf(path, sizeof path, "%s/c-intoslice-in.h5", workdir);
write_contig(path, data, CONTIG_N);
free(data);
double *buf = malloc(PIECE * sizeof(double));
hid_t f = H5Fopen(path, H5F_ACC_RDONLY, H5P_DEFAULT);
hid_t d = H5Dopen2(f, "data", H5P_DEFAULT);
hid_t fsp = H5Dget_space(d);
hsize_t count = PIECE;
hid_t msp = H5Screate_simple(1, &count, NULL);
hsize_t off0 = 0;
CHECK(H5Sselect_hyperslab(fsp, H5S_SELECT_SET, &off0, NULL, &count, NULL));
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, msp, fsp, H5P_DEFAULT, buf));
TIMED({
for (int k = 0; k < CONTIG_N / PIECE; k++) {
hsize_t off = (hsize_t)k * PIECE;
CHECK(H5Sselect_hyperslab(fsp, H5S_SELECT_SET, &off, NULL,
&count, NULL));
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, msp, fsp, H5P_DEFAULT, buf));
}
if (buf[PIECE - 1] != (double)(CONTIG_N - 1)) abort();
});
H5Sclose(msp);
H5Sclose(fsp);
H5Dclose(d);
H5Fclose(f);
free(buf);
} else if (strcmp(wl, "chunked-write") == 0) {
double *data = ramp(CONTIG_N);
snprintf(path, sizeof path, "%s/c-chunked.h5", workdir);
Expand All @@ -200,6 +249,56 @@ int main(int argc, char **argv) {
snprintf(path, sizeof path, "%s/c-chunked-in.h5", workdir);
write_chunked(path, data, CONTIG_N, 0, CHUNK_ELEMS);
TIMED({ free(read_full(path, CONTIG_N)); });
} else if (strcmp(wl, "chunked-into-read") == 0) {
/* The buffer-reuse pair again, on an unfiltered chunked dataset. */
double *data = ramp(CONTIG_N);
snprintf(path, sizeof path, "%s/c-chunkintoread-in.h5", workdir);
write_chunked(path, data, CONTIG_N, 0, CHUNK_ELEMS);
free(data);
double *buf = malloc(CONTIG_N * sizeof(double));
hid_t f = H5Fopen(path, H5F_ACC_RDONLY, H5P_DEFAULT);
hid_t d = H5Dopen2(f, "data", H5P_DEFAULT);
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf));
TIMED({
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf));
if (buf[CONTIG_N - 1] != (double)(CONTIG_N - 1)) abort();
});
H5Dclose(d);
H5Fclose(f);
free(buf);
} else if (strcmp(wl, "chunked-into-slice") == 0) {
/* slice-read's random 64 KiB selections, into a kept buffer. */
double *data = ramp(CONTIG_N);
snprintf(path, sizeof path, "%s/c-chunkintoslice-in.h5", workdir);
write_chunked(path, data, CONTIG_N, 0, CHUNK_ELEMS);
free(data);
double *buf = malloc(SLICE_ELEMS * sizeof(double));
hid_t f = H5Fopen(path, H5F_ACC_RDONLY, H5P_DEFAULT);
hid_t d = H5Dopen2(f, "data", H5P_DEFAULT);
hid_t fsp = H5Dget_space(d);
hsize_t count = SLICE_ELEMS;
hid_t msp = H5Screate_simple(1, &count, NULL);
hsize_t off0 = 0;
CHECK(H5Sselect_hyperslab(fsp, H5S_SELECT_SET, &off0, NULL, &count, NULL));
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, msp, fsp, H5P_DEFAULT, buf));
TIMED({
lcg_state = 1;
hsize_t last = 0;
for (int k = 0; k < SLICE_READS; k++) {
hsize_t off = lcg_next() % (CONTIG_N - SLICE_ELEMS);
CHECK(H5Sselect_hyperslab(fsp, H5S_SELECT_SET, &off, NULL,
&count, NULL));
CHECK(H5Dread(d, H5T_NATIVE_DOUBLE, msp, fsp, H5P_DEFAULT,
buf));
last = off;
}
if (buf[0] != (double)last) abort();
});
H5Sclose(msp);
H5Sclose(fsp);
H5Dclose(d);
H5Fclose(f);
free(buf);
} else if (strcmp(wl, "deflate-write") == 0) {
double *data = compressible(DEFLATE_N);
snprintf(path, sizeof path, "%s/c-deflate.h5", workdir);
Expand Down
8 changes: 8 additions & 0 deletions perf/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@
# workload is "obtain the data and sum it", and the view is what changes
# between builds. See src/bin/perf_probe.rs.
("contig-view", 5),
# Buffer-reuse steady state: open and the first read are setup, timed
# reads land in an already-faulted buffer.
("into-read", 5),
("into-slice", 5),
("chunked-write", 5),
("chunked-read", 5),
# The buffer-reuse pair on an unfiltered chunked dataset: full rereads
# and slice-read's random selections into a kept buffer.
("chunked-into-read", 5),
("chunked-into-slice", 5),
("deflate-write", 3),
("deflate-read", 5),
("deflate-slice", 5),
Expand Down
70 changes: 70 additions & 0 deletions src/bin/perf_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,41 @@ fn main() {
std::hint::black_box(total);
});
}
// The buffer-reuse pattern: the destination is faulted in once, so
// every timed read pays only for moving bytes, not for growing a
// fresh allocation. Open and the first (untimed) read happen in
// setup — this times the steady state of a reader that keeps its
// buffer.
"into-read" => {
let path = p("rs-intoread-in.h5");
write_contig(&path, &ramp(CONTIG_N));
let file = H5File::open(&path).unwrap();
let ds = file.dataset("data").unwrap();
let mut buf = vec![0f64; CONTIG_N];
ds.read_raw_into(&mut buf).unwrap();
timed(&workload, reps, || {
ds.read_raw_into(&mut buf).unwrap();
assert_eq!(buf[CONTIG_N - 1], (CONTIG_N - 1) as f64);
});
}
// The same reuse pattern per piece: sequential 128 KiB slices into
// one buffer, covering the dataset once per rep.
"into-slice" => {
const PIECE: usize = 16 * 1024; // f64 -> 128 KiB
let path = p("rs-intoslice-in.h5");
write_contig(&path, &ramp(CONTIG_N));
let file = H5File::open(&path).unwrap();
let ds = file.dataset("data").unwrap();
let mut buf = vec![0f64; PIECE];
ds.read_slice_into(&mut buf, &[0], &[PIECE]).unwrap();
timed(&workload, reps, || {
for k in 0..CONTIG_N / PIECE {
ds.read_slice_into(&mut buf, &[k * PIECE], &[PIECE])
.unwrap();
}
assert_eq!(buf[PIECE - 1], (CONTIG_N - 1) as f64);
});
}
"chunked-write" => {
let data = ramp(CONTIG_N);
let path = p("rs-chunked.h5");
Expand All @@ -155,6 +190,41 @@ fn main() {
assert_eq!(v.len(), CONTIG_N);
});
}
// The buffer-reuse pair again, on an unfiltered chunked dataset:
// the chunk runs land straight in the kept buffer, never
// materializing a chunk image.
"chunked-into-read" => {
let path = p("rs-chunkintoread-in.h5");
write_chunked(&path, &ramp(CONTIG_N), false, CHUNK_ELEMS);
let file = H5File::open(&path).unwrap();
let ds = file.dataset("data").unwrap();
let mut buf = vec![0f64; CONTIG_N];
ds.read_raw_into(&mut buf).unwrap();
timed(&workload, reps, || {
ds.read_raw_into(&mut buf).unwrap();
assert_eq!(buf[CONTIG_N - 1], (CONTIG_N - 1) as f64);
});
}
// slice-read's random 64 KiB selections, into a kept buffer.
"chunked-into-slice" => {
let path = p("rs-chunkintoslice-in.h5");
write_chunked(&path, &ramp(CONTIG_N), false, CHUNK_ELEMS);
let file = H5File::open(&path).unwrap();
let ds = file.dataset("data").unwrap();
let mut buf = vec![0f64; SLICE_ELEMS];
ds.read_slice_into(&mut buf, &[0], &[SLICE_ELEMS]).unwrap();
timed(&workload, reps, || {
let mut rng = Lcg(1);
let mut last = 0usize;
for _ in 0..SLICE_READS {
let off = (rng.next() % (CONTIG_N - SLICE_ELEMS) as u64) as usize;
ds.read_slice_into(&mut buf, &[off], &[SLICE_ELEMS])
.unwrap();
last = off;
}
assert_eq!(buf[0], last as f64);
});
}
"deflate-write" => {
let data = compressible(DEFLATE_N);
let path = p("rs-deflate.h5");
Expand Down
13 changes: 10 additions & 3 deletions src/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::format::messages::virtual_mapping::VirtualMapping;
use crate::format::reference::{Reference, ReferenceTarget};
use crate::format::selection::Selection;
use crate::format::storage_kind::AttributeStorage;
use crate::io::file_handle::ReadDst;
use crate::io::reader::{read_image_into_new, ExternalFileSegment};
use crate::io::writer::ChunkIndexKind;
use crate::types::H5Type;
Expand Down Expand Up @@ -3811,7 +3812,13 @@ impl H5Dataset {
// are touched once instead of being read into a byte buffer and
// copied into a second one of the same size.
read_image_into_new(count, |image| {
reader.read_slice_into(name, &starts_u64, &counts_u64, image)?;
reader.read_slice_into_dst(
name,
&starts_u64,
&counts_u64,
image,
ReadDst::Fresh,
)?;
to_host_byte_order(image, &datatype, T::element_size())
})
}
Expand Down Expand Up @@ -3928,7 +3935,7 @@ impl H5Dataset {
return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
};
read_image_into_new(points_u64.len(), |image| {
reader.read_points_into(name, &points_u64, image)?;
reader.read_points_into(name, &points_u64, image, ReadDst::Fresh)?;
to_host_byte_order(image, &datatype, T::element_size())
})
}
Expand Down Expand Up @@ -4419,7 +4426,7 @@ impl H5Dataset {
// bytes are touched once rather than being zeroed, read, and
// then copied into a second buffer of the same size.
read_image_into_new(total / T::element_size(), |image| {
reader.read_dataset_raw_into(name, image)?;
reader.read_dataset_raw_into_dst(name, image, ReadDst::Fresh)?;
to_host_byte_order(image, &datatype, T::element_size())
})
}
Expand Down
Loading
Loading