perf(format): parallel cached decode and fewer copies on full reads

Same-moment A/B on a 64 MB f64 dataset: chunked+deflate 110 -> 69 ms, chunked
72 -> 60 ms, contiguous 56 -> 30 ms.

- read_chunked_data_cached — the path the facade uses — decompressed chunks
  one at a time; only the uncached reader was parallel. Cache misses are now
  decoded in bounded batches (128), in parallel with the `parallel` feature.
- Every chunk was pushed into the 16 MiB chunk cache, which a larger dataset
  just churns (insert, evict moments later). Chunks are cached only when the
  whole dataset fits (new ChunkCache::max_bytes).
- Unfiltered chunks went file -> Vec -> aligned cache buffer -> output. They
  are copied straight from the file bytes.
- The facade's typed reads convert a contiguous dataset straight from the
  borrowed file bytes instead of copying it into a Vec first.
- The native little-endian fast paths allocated vec![0; n] and then overwrote
  it; they now fill an uninitialised buffer in one copy (native_le_to_vec).
  alloc_output requests zeroed memory from the allocator instead of reserving
  and filling.

The unit test that expected unfiltered chunks to land in the decompressed
cache now asserts the new design (index reused, cache not involved).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 14:05:15 -07:00
co-authored by Claude Fable 5.1
parent d668e45ab5
commit 0addf328bc
6 changed files with 197 additions and 80 deletions
+30 -5
View File
@@ -381,8 +381,13 @@ impl<'f> Dataset<'f> {
/// Read all data as `f64` values.
pub fn read_f64(&self) -> Result<Vec<f64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
// A contiguous dataset is converted straight from the file bytes; going
// through `read_raw` first copied the whole dataset an extra time.
if let Ok(Some(bytes)) = self.read_raw_ref() {
return Ok(data_read::read_as_f64(bytes, &dt)?);
}
let raw = self.read_raw()?;
Ok(data_read::read_as_f64(&raw, &dt)?)
}
@@ -393,29 +398,49 @@ impl<'f> Dataset<'f> {
///
/// Read all data as `f32` values.
pub fn read_f32(&self) -> Result<Vec<f32>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
// A contiguous dataset is converted straight from the file bytes; going
// through `read_raw` first copied the whole dataset an extra time.
if let Ok(Some(bytes)) = self.read_raw_ref() {
return Ok(data_read::read_as_f32(bytes, &dt)?);
}
let raw = self.read_raw()?;
Ok(data_read::read_as_f32(&raw, &dt)?)
}
/// Read all data as `i32` values.
pub fn read_i32(&self) -> Result<Vec<i32>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
// A contiguous dataset is converted straight from the file bytes; going
// through `read_raw` first copied the whole dataset an extra time.
if let Ok(Some(bytes)) = self.read_raw_ref() {
return Ok(data_read::read_as_i32(bytes, &dt)?);
}
let raw = self.read_raw()?;
Ok(data_read::read_as_i32(&raw, &dt)?)
}
/// Read all data as `i64` values.
pub fn read_i64(&self) -> Result<Vec<i64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
// A contiguous dataset is converted straight from the file bytes; going
// through `read_raw` first copied the whole dataset an extra time.
if let Ok(Some(bytes)) = self.read_raw_ref() {
return Ok(data_read::read_as_i64(bytes, &dt)?);
}
let raw = self.read_raw()?;
Ok(data_read::read_as_i64(&raw, &dt)?)
}
/// Read all data as `u64` values.
pub fn read_u64(&self) -> Result<Vec<u64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
// A contiguous dataset is converted straight from the file bytes; going
// through `read_raw` first copied the whole dataset an extra time.
if let Ok(Some(bytes)) = self.read_raw_ref() {
return Ok(data_read::read_as_u64(bytes, &dt)?);
}
let raw = self.read_raw()?;
Ok(data_read::read_as_u64(&raw, &dt)?)
}