clawhdf5: parse local files through the slice, and cache the contiguous view

Since M2 the facade handed the metadata parsers its FileData view, so a
local file ran the parsers monomorphised for FileData, whose Storage impl
worked out contiguous() (patched and overlay checks, two range conversions)
on every structure read. A metadata walk of h5stat_newgrat.h5 (35,001
groups: open, entries and attrs of each) was about 4.5% slower than at
8f59b2e.

- FileData works out its contiguous slice once at open (a borrow of its own
  heap/mapped buffer, kept as a pointer; see the SAFETY notes).
- with_bytes! hands the in-memory slice to the format parsers when the file
  has one (header parsing, attributes, group listings and lookups, path
  resolution, shared messages, VL decoding), so local files run the [u8]
  parsers as before; storage-backed files still get FileData.

Provisional A/B on tank (load 5-11), best of 30, 5 alternating rounds:
walk 26.07-26.37 ms at 8f59b2e, 27.24-28.03 ms before this commit,
26.52-26.98 ms after. Caching alone did not move it (27.02-27.47 ms); the
dispatch did. File::open read_f32 on 32M f32 (contiguous, chunked, gzip)
stays within noise of 8f59b2e.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 18:41:10 -05:00
co-authored by Claude Opus 5.5
parent b086dc3c2b
commit 89e7977943
+122 -40
View File
@@ -64,6 +64,24 @@ impl Backing {
} }
} }
/// Evaluate `$body` with `$d` bound to the bytes the `clawhdf5_format`
/// parsers read: the in-memory slice when the file has one (a `Vec`, an
/// mmap), so local files run the parsers monomorphised for `[u8]` — the
/// slice code, as before the range-read migration — and otherwise the
/// [`FileData`] itself, whose reads go to the storage.
macro_rules! with_bytes {
($data:expr, |$d:ident| $body:expr) => {{
let data: &FileData = $data;
match data.contiguous() {
Some($d) => $body,
None => {
let $d = data;
$body
}
}
}};
}
/// The file's bytes, viewed from the superblock on and up to the end of /// The file's bytes, viewed from the superblock on and up to the end of
/// file the superblock records. A file may start with a user block (the /// file the superblock records. A file may start with a user block (the
/// superblock at 512, 1024, …); every HDF5 address is relative to the /// superblock at 512, 1024, …); every HDF5 address is relative to the
@@ -93,8 +111,28 @@ struct FileData {
/// then); every object lookup here fails with this error, and no /// then); every object lookup here fails with this error, and no
/// metadata is read from the file's own, possibly stale, bytes. /// metadata is read from the file's own, possibly stale, bytes.
image_error: Option<FormatError>, image_error: Option<FormatError>,
/// [`Self::contiguous`], worked out once at open: every structure a
/// parser reads asks for it, and the patched/overlay checks and range
/// conversions behind it cost a local metadata walk a few percent.
contiguous: Option<WholeView>,
} }
/// A borrow of the HDF5 data held by a [`FileData`]'s own `backing` or
/// `patched` buffer (see [`FileData::contiguous`]).
#[derive(Clone, Copy)]
struct WholeView {
ptr: *const u8,
len: usize,
}
// SAFETY: a `WholeView` is only a borrow of bytes owned (through `backing`
// or `patched`) by the `FileData` holding it, which is `Send + Sync`: the
// bytes are never written after open, so sharing the pointer across
// threads is sharing a `&[u8]`.
unsafe impl Send for WholeView {}
// SAFETY: as above.
unsafe impl Sync for WholeView {}
impl FileData { impl FileData {
/// Locate the superblock and parse it. A truncated file is refused, and /// Locate the superblock and parse it. A truncated file is refused, and
/// bytes past the recorded end of file are not read, as in libhdf5. /// bytes past the recorded end of file are not read, as in libhdf5.
@@ -127,17 +165,17 @@ impl FileData {
ImageView::Patched(p) => (Some(p), None), ImageView::Patched(p) => (Some(p), None),
ImageView::Unloadable(e) => (None, Some(e)), ImageView::Unloadable(e) => (None, Some(e)),
}; };
Ok(( let mut data = Self {
Self {
backing, backing,
base: base as u64, base: base as u64,
end: end as u64, end: end as u64,
patched, patched,
overlay: Vec::new(), overlay: Vec::new(),
image_error, image_error,
}, contiguous: None,
superblock, };
)) data.contiguous = data.find_contiguous();
Ok((data, superblock))
} }
/// [`Self::new`] for a [`Storage`] backend: the same checks, through /// [`Self::new`] for a [`Storage`] backend: the same checks, through
@@ -152,7 +190,11 @@ impl FileData {
patched: None, patched: None,
overlay: Vec::new(), overlay: Vec::new(),
image_error: None, image_error: None,
contiguous: None,
}; };
// Worked out again below, once the end of file and any cache image
// are known.
data.contiguous = data.find_contiguous();
let superblock = Superblock::parse_in(&data, 0)?; let superblock = Superblock::parse_in(&data, 0)?;
data.end = base + superblock.data_end(base, file_len)?; data.end = base + superblock.data_end(base, file_len)?;
match superblock_ext::cache_image_state_in(&data, &superblock)? { match superblock_ext::cache_image_state_in(&data, &superblock)? {
@@ -167,13 +209,37 @@ impl FileData {
.collect(); .collect();
} }
} }
data.contiguous = data.find_contiguous();
Ok((data, superblock)) Ok((data, superblock))
} }
/// The HDF5 data as one slice, when the file is in memory (a `Vec`, an /// The HDF5 data as one slice, when the file is in memory (a `Vec`, an
/// mmap, or a storage that holds it all and has no cache image to lay /// mmap, or a storage that holds it all and has no cache image to lay
/// over it). /// over it).
#[inline]
fn contiguous(&self) -> Option<&[u8]> { fn contiguous(&self) -> Option<&[u8]> {
// SAFETY: `find_contiguous` borrowed these bytes from `backing` or
// `patched`, which this `FileData` owns and never changes after
// open. They live on the heap or in a mapping (a `Vec`'s buffer, an
// mmap, a private copy, or a buffer inside the `Arc`'d storage), so
// they stay put when the `FileData` moves, and they live as long as
// `self`.
self.contiguous
.map(|v| unsafe { core::slice::from_raw_parts(v.ptr, v.len) })
}
/// [`Self::contiguous`], worked out from `backing` and `patched`.
fn find_contiguous(&self) -> Option<WholeView> {
let bytes = self.compute_contiguous()?;
Some(WholeView {
ptr: bytes.as_ptr(),
len: bytes.len(),
})
}
/// The HDF5 data as one slice, from `backing` and `patched` (see
/// [`Self::contiguous`]).
fn compute_contiguous(&self) -> Option<&[u8]> {
if let Some(p) = &self.patched { if let Some(p) = &self.patched {
return p.get(usize::try_from(self.base).ok()?..usize::try_from(self.end).ok()?); return p.get(usize::try_from(self.base).ok()?..usize::try_from(self.end).ok()?);
} }
@@ -224,6 +290,7 @@ impl FileData {
} }
impl Storage for FileData { impl Storage for FileData {
#[inline]
fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> { fn read_at(&self, offset: u64, len: usize) -> Result<Cow<'_, [u8]>, FormatError> {
if let Some(all) = self.contiguous() { if let Some(all) = self.contiguous() {
return all.read_at(offset, len); return all.read_at(offset, len);
@@ -261,6 +328,7 @@ impl Storage for FileData {
.collect()) .collect())
} }
#[inline]
fn as_contiguous(&self) -> Option<&[u8]> { fn as_contiguous(&self) -> Option<&[u8]> {
self.contiguous() self.contiguous()
} }
@@ -400,8 +468,11 @@ impl File {
/// ///
/// The path uses `/` separators (e.g., `"group1/values"`). /// The path uses `/` separators (e.g., `"group1/values"`).
pub fn dataset(&self, path: &str) -> Result<Dataset<'_>, Error> { pub fn dataset(&self, path: &str) -> Result<Dataset<'_>, Error> {
let data = self.data.meta()?; let addr = with_bytes!(self.data.meta()?, |d| group_v2::resolve_path_any_in(
let addr = group_v2::resolve_path_any_in(data, &self.superblock, path)?; d,
&self.superblock,
path
))?;
let hdr = self.parse_header(addr)?; let hdr = self.parse_header(addr)?;
if !has_message(&hdr, MessageType::DataLayout) { if !has_message(&hdr, MessageType::DataLayout) {
return Err(Error::NotADataset(path.to_string())); return Err(Error::NotADataset(path.to_string()));
@@ -447,8 +518,11 @@ impl File {
/// The path uses `/` separators (e.g., `"sensors"`). /// The path uses `/` separators (e.g., `"sensors"`).
/// Use `"/"` or `""` for the root group. /// Use `"/"` or `""` for the root group.
pub fn group(&self, path: &str) -> Result<Group<'_>, Error> { pub fn group(&self, path: &str) -> Result<Group<'_>, Error> {
let data = self.data.meta()?; let addr = with_bytes!(self.data.meta()?, |d| group_v2::resolve_path_any_in(
let addr = group_v2::resolve_path_any_in(data, &self.superblock, path)?; d,
&self.superblock,
path
))?;
Ok(Group { Ok(Group {
file: self, file: self,
address: addr, address: addr,
@@ -560,13 +634,13 @@ impl File {
/// [`AttrValue::Raw`] attribute. Variable-length strings are resolved in /// [`AttrValue::Raw`] attribute. Variable-length strings are resolved in
/// this file's global heap; see [`Dataset::read_string`] for the values. /// this file's global heap; see [`Dataset::read_string`] for the values.
pub fn decode_strings(&self, datatype: &Datatype, raw: &[u8]) -> Result<Vec<String>, Error> { pub fn decode_strings(&self, datatype: &Datatype, raw: &[u8]) -> Result<Vec<String>, Error> {
crate::vlen::decode_strings( with_bytes!(&self.data, |d| crate::vlen::decode_strings(
&self.data, d,
datatype, datatype,
raw, raw,
self.offset_size(), self.offset_size(),
self.length_size(), self.length_size(),
) ))
} }
/// Like [`decode_strings`](Self::decode_strings) for variable-length /// Like [`decode_strings`](Self::decode_strings) for variable-length
@@ -577,13 +651,13 @@ impl File {
datatype: &Datatype, datatype: &Datatype,
raw: &[u8], raw: &[u8],
) -> Result<Vec<Vec<u8>>, Error> { ) -> Result<Vec<Vec<u8>>, Error> {
crate::vlen::decode_string_bytes( with_bytes!(self.data.meta()?, |d| crate::vlen::decode_string_bytes(
self.data.meta()?, d,
datatype, datatype,
raw, raw,
self.offset_size(), self.offset_size(),
self.length_size(), self.length_size(),
) ))
} }
/// Decode the variable-length sequences in `raw`, a buffer of elements /// Decode the variable-length sequences in `raw`, a buffer of elements
@@ -595,22 +669,22 @@ impl File {
datatype: &Datatype, datatype: &Datatype,
raw: &[u8], raw: &[u8],
) -> Result<Vec<Vec<T>>, Error> { ) -> Result<Vec<Vec<T>>, Error> {
crate::vlen::decode_vlen( with_bytes!(self.data.meta()?, |d| crate::vlen::decode_vlen(
self.data.meta()?, d,
datatype, datatype,
raw, raw,
self.offset_size(), self.offset_size(),
self.length_size(), self.length_size(),
) ))
} }
fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> { fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> {
ObjectHeader::parse_in( with_bytes!(self.data.meta()?, |d| ObjectHeader::parse_in(
self.data.meta()?, d,
address, address,
self.superblock.offset_size, self.superblock.offset_size,
self.superblock.length_size, self.superblock.length_size,
) ))
} }
fn offset_size(&self) -> u8 { fn offset_size(&self) -> u8 {
@@ -688,8 +762,12 @@ impl<'f> Group<'f> {
&self, &self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> { ) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let data = &self.file.data; with_bytes!(&self.file.data, |d| read_attrs(
read_attrs(data, &hdr, self.file.offset_size(), self.file.length_size()) d,
&hdr,
self.file.offset_size(),
self.file.length_size()
))
} }
/// Get a dataset within this group by name. /// Get a dataset within this group by name.
@@ -719,14 +797,13 @@ impl<'f> Group<'f> {
/// stored densely. /// stored densely.
pub fn attr(&self, name: &str) -> Result<Option<AttrValue>, Error> { pub fn attr(&self, name: &str) -> Result<Option<AttrValue>, Error> {
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let data = &self.file.data; with_bytes!(&self.file.data, |d| read_attr(
read_attr( d,
data,
&hdr, &hdr,
name, name,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
) ))
} }
/// The object header address of the child called `name`: the entry of /// The object header address of the child called `name`: the entry of
@@ -734,8 +811,12 @@ impl<'f> Group<'f> {
/// name index rather than by listing the group (see /// name index rather than by listing the group (see
/// [`group_v2::resolve_child`]). /// [`group_v2::resolve_child`]).
fn child_address(&self, name: &str) -> Result<u64, Error> { fn child_address(&self, name: &str) -> Result<u64, Error> {
let data = self.file.data.meta()?; with_bytes!(self.file.data.meta()?, |d| group_v2::resolve_child_in(
group_v2::resolve_child_in(data, &self.file.superblock, self.address, name) d,
&self.file.superblock,
self.address,
name
))
.map_err(Error::Format) .map_err(Error::Format)
} }
@@ -757,8 +838,9 @@ impl<'f> Group<'f> {
/// [`group_v2::resolve_group_children`]); dangling, external and /// [`group_v2::resolve_group_children`]); dangling, external and
/// user-defined links are left out. /// user-defined links are left out.
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let data = self.file.data.meta()?; with_bytes!(self.file.data.meta()?, |d| {
group_v2::resolve_group_children_in(data, &self.file.superblock, self.address) group_v2::resolve_group_children_in(d, &self.file.superblock, self.address)
})
.map_err(Error::Format) .map_err(Error::Format)
} }
} }
@@ -1332,13 +1414,12 @@ impl<'f> Dataset<'f> {
pub fn attrs_with_errors( pub fn attrs_with_errors(
&self, &self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> { ) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let data = &self.file.data; with_bytes!(&self.file.data, |d| read_attrs(
read_attrs( d,
data,
&self.header, &self.header,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
) ))
} }
/// The attribute called `name`, or `None` if it has none by that name /// The attribute called `name`, or `None` if it has none by that name
@@ -1346,14 +1427,13 @@ impl<'f> Dataset<'f> {
/// that name, found without reading the other attributes when they are /// that name, found without reading the other attributes when they are
/// stored densely. /// stored densely.
pub fn attr(&self, name: &str) -> Result<Option<AttrValue>, Error> { pub fn attr(&self, name: &str) -> Result<Option<AttrValue>, Error> {
let data = &self.file.data; with_bytes!(&self.file.data, |d| read_attr(
read_attr( d,
data,
&self.header, &self.header,
name, name,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
) ))
} }
/// Verify this dataset's content against its stored provenance hash /// Verify this dataset's content against its stored provenance hash
@@ -1393,12 +1473,14 @@ impl<'f> Dataset<'f> {
.iter() .iter()
.find(|m| m.msg_type == msg_type) .find(|m| m.msg_type == msg_type)
.map(|msg| { .map(|msg| {
with_bytes!(&self.file.data, |d| {
clawhdf5_format::shared_message::message_data_in( clawhdf5_format::shared_message::message_data_in(
&self.file.data, d,
msg, msg,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
) )
})
.map_err(Error::Format) .map_err(Error::Format)
}) })
.transpose() .transpose()