perf(py): read an index list one group of chunks at a time

Each run of consecutive indices was its own uncached hyperslab read, so
a list over a compressed chunked dataset decoded the same chunk once per
run (d[range(0, 200000, 40)] over 20 gzip chunks: 8 s, h5py 0.014 s).
Plan::reads now groups the indices — a group ends only where a whole
chunk holds no selected index, or, unchunked, at a gap over 64 KiB — and
the selected rows are gathered from each group's block in Rust. Now
3.8 ms (h5py 4.1 ms, release, tank). The new test (1-D, 2-D and
contiguous, compared with h5py, 2 s bound) took 5.8 s before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 08:57:16 -05:00
co-authored by Claude Opus 5.5
parent 8c51b05b9c
commit b43bd2e67f
5 changed files with 259 additions and 36 deletions
+24 -4
View File
@@ -31,6 +31,8 @@ pub struct PyDataset {
path: String,
/// `None` for a dataset with a null dataspace (h5py's `Empty`).
shape: Option<Vec<u64>>,
/// The chunk shape, for a chunked dataset.
chunks: Option<Vec<u64>>,
datatype: Datatype,
/// Why the datatype cannot be read into numpy, if it cannot.
conv: Result<Converter, String>,
@@ -56,10 +58,14 @@ impl PyDataset {
};
let conv = Converter::new(py, &datatype, file.superblock().offset_size)
.map_err(|e| e.value(py).to_string());
let chunks = shape
.as_ref()
.and_then(|s| node::chunk_shape(&file, &hdr, s.len()));
Ok(Self {
file,
path,
shape,
chunks,
datatype,
conv,
})
@@ -81,17 +87,23 @@ impl PyDataset {
let arr = if plan.is_empty() {
conv.empty(py, &out_shape)?
} else {
let (reads, list_axis) = plan.reads(dims);
let (vl, elem_size, unit) = (conv.is_vl(), conv.elem_size, conv.vl_unit);
let list_axis = plan.list_axis();
let chunk_len = match (&self.chunks, list_axis) {
(Some(c), Some(a)) => c.get(a).copied(),
_ => None,
};
let (reads, list_axis) = plan.reads(dims, chunk_len, elem_size);
let read_shape = plan.read_shape();
let file = &*self.file;
let path = self.path.as_str();
let (vl, elem_size, unit) = (conv.is_vl(), conv.elem_size, conv.vl_unit);
// Everything below touches only Rust data: release the GIL.
let read = || -> Result<Elements, ReadError> {
let ds = file.dataset(path)?;
let mut blocks = Vec::with_capacity(reads.len());
for (sel, shape) in reads {
let raw = ds.read_selection(&sel)?;
for read in reads {
let raw = ds.read_selection(&read.sel)?;
let mut shape = read.shape;
let want = shape.iter().product::<usize>() * elem_size;
if raw.len() != want {
return Err(ReadError::Other(format!(
@@ -99,6 +111,14 @@ impl PyDataset {
raw.len()
)));
}
let raw = match (&read.pick, list_axis) {
(Some(pick), Some(axis)) => {
let kept = select::gather_along(&raw, &shape, axis, pick, elem_size);
shape[axis] = pick.len();
kept
}
_ => raw,
};
blocks.push((raw, shape));
}
// Several blocks only for a list index: join their bytes