block: qcow: Refactor pointer table writes to use iterators

Refactor write_pointer_table to accept iterators instead of requiring
materialized vectors, eliminating temporary allocations in L1 table
sync operations.

Changes:
- Modified write_pointer_table() to take Iterator<Item = &T> and
  dereference internally before passing owned values to the callback
- Added write_pointer_table_direct() convenience wrapper for cases
  without value transformation
- Updated sync_caches() to use l1_table.iter() directly instead of
  .get_values().iter().copied()
- Implemented Deref<Target = [T]> for VecCache to enable direct .iter()

Performance impact:
- Eliminates L1 table allocation during sync (~2KB per 100GB disk)
- L2 and refcount table writes already used slices, no change there
- Zero performance overhead: iterator dereferencing is equivalent to
  .copied() and optimizes identically

The L1 sync previously collected entries into a Vec to apply the
OFLAG_COPIED flag. The new iterator+callback pattern computes this
on-the-fly, avoiding the allocation entirely.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2025-12-10 10:06:07 +01:00
committed by Bo Chen
parent be5b14ef3a
commit 5c5f33050c
3 changed files with 54 additions and 37 deletions

View File

@@ -773,7 +773,7 @@ impl QcowFile {
let raw_file = &mut self.raw_file;
self.l2_cache
.insert(l1_index, table, |index, evicted| {
raw_file.write_pointer_table(l1_table[index], evicted.get_values(), 0)
raw_file.write_pointer_table_direct(l1_table[index], evicted.iter())
})
.map_err(Error::EvictingCache)?;
}
@@ -998,7 +998,7 @@ impl QcowFile {
// Rewrite the top-level refcount table.
raw_file
.write_pointer_table(header.refcount_table_offset, ref_table, 0)
.write_pointer_table_direct(header.refcount_table_offset, ref_table.iter())
.map_err(Error::WritingHeader)?;
// Rewrite the header again, now with lazy refcounts disabled.
@@ -1513,7 +1513,7 @@ impl QcowFile {
let l1_table = &self.l1_table;
let raw_file = &mut self.raw_file;
self.l2_cache.insert(l1_index, l2_table, |index, evicted| {
raw_file.write_pointer_table(l1_table[index], evicted.get_values(), 0)
raw_file.write_pointer_table_direct(l1_table[index], evicted.iter())
})?;
}
Ok(new_cluster)
@@ -1596,7 +1596,7 @@ impl QcowFile {
let addr = self.l1_table[*l1_index];
if addr != 0 {
self.raw_file
.write_pointer_table(addr, l2_table.get_values(), 0)?;
.write_pointer_table_direct(addr, l2_table.iter())?;
} else {
return Err(std::io::Error::from_raw_os_error(EINVAL));
}
@@ -1610,25 +1610,22 @@ impl QcowFile {
// Push L1 table and refcount table last as all the clusters they point to are now
// guaranteed to be valid.
let mut sync_required = if self.l1_table.dirty() {
// Build L1 table with OFLAG_COPIED bits set correctly based on L2 cluster refcounts
let l1_active: Vec<u64> = self
.l1_table
.get_values()
.iter()
.map(|&l2_addr| {
// Write L1 table with OFLAG_COPIED bits
let refcounts = &mut self.refcounts;
self.raw_file.write_pointer_table(
self.header.l1_table_offset,
self.l1_table.iter(),
|raw_file, l2_addr| {
if l2_addr == 0 {
Ok(0)
} else {
let refcount = self
.refcounts
.get_cluster_refcount(&mut self.raw_file, l2_addr)
let refcount = refcounts
.get_cluster_refcount(raw_file, l2_addr)
.map_err(|e| std::io::Error::other(Error::GettingRefcount(e)))?;
Ok(l1_entry_make(l2_addr, refcount == 1))
}
})
.collect::<io::Result<Vec<u64>>>()?;
self.raw_file
.write_pointer_table(self.header.l1_table_offset, &l1_active, 0)?;
},
)?;
self.l1_table.mark_clean();
true
} else {

View File

@@ -61,24 +61,47 @@ impl QcowRawFile {
self.read_pointer_table(offset, count, mask)
}
/// Writes `table` of u64 pointers to `offset` in the file.
/// `non_zero_flags` will be ORed with all non-zero values in `table`.
/// writing.
pub fn write_pointer_table(
/// Internal helper for creating a buffered writer for pointer tables.
#[inline]
fn setup_pointer_table_writer<T>(
&mut self,
offset: u64,
table: &[u64],
non_zero_flags: u64,
) -> io::Result<()> {
entries: &impl Iterator<Item = T>,
) -> io::Result<BufWriter<RawFile>> {
self.file.seek(SeekFrom::Start(offset))?;
let mut buffer = BufWriter::with_capacity(std::mem::size_of_val(table), &mut self.file);
for addr in table {
let val = if *addr == 0 {
0
} else {
*addr | non_zero_flags
};
buffer.write_u64::<BigEndian>(val)?;
let my_file = self.file.try_clone()?;
let capacity = entries.size_hint().0 * size_of::<u64>();
Ok(BufWriter::with_capacity(capacity, my_file))
}
/// Writes a pointer table to `offset` in the file.
/// Entries are computed on-the-fly by the callback.
pub fn write_pointer_table<'a, T: Copy + 'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a T>,
mut f: impl FnMut(&mut QcowRawFile, T) -> io::Result<u64>,
) -> io::Result<()> {
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
for addr in entries {
let entry = f(self, *addr)?;
buffer.write_u64::<BigEndian>(entry)?;
}
buffer.flush()?;
Ok(())
}
/// Writes a pointer table directly without transforming values.
pub fn write_pointer_table_direct<'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a u64>,
) -> io::Result<()> {
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
for &entry in entries {
buffer.write_u64::<BigEndian>(entry)?;
}
buffer.flush()?;
Ok(())

View File

@@ -156,11 +156,8 @@ impl RefCount {
/// Returns true if the table changed since the previous `flush_table()` call.
pub fn flush_table(&mut self, raw_file: &mut QcowRawFile) -> io::Result<bool> {
if self.ref_table.dirty() {
raw_file.write_pointer_table(
self.refcount_table_offset,
self.ref_table.get_values(),
0,
)?;
raw_file
.write_pointer_table_direct(self.refcount_table_offset, self.ref_table.iter())?;
self.ref_table.mark_clean();
Ok(true)
} else {