read: hold the space table across calls, keyed on the generation
Every coordinate read binary-searches the space table for its space's first record, and every batch of a listing reads a row of it — from the device, per probe, for data that changes only when the store is folded. It is now held in the driver and keyed on the **control block's generation**, which a fold raises along with everything else it writes, plus the image offset, since a fold flips the slot. Nothing has to invalidate it by hand: a writer that bumps the generation invalidates it, including a writer this driver never sees. A stale table cannot outlive the image it describes. Honest about what it bought: nothing measurable in the gate, which is the interesting part. The gate's store carries about a dozen spaces, so the search it replaces was three or four probes, and copying a few hundred bytes of table costs about what those probes did — get 0.078 -> 0.077 ms, spaces flat within the run-to-run noise of this bench (which is ~20%). What changes is the SHAPE: the lookup no longer scales with the number of spaces, and the box has 21 and grows. That is worth having and it is not the same thing as a measured win, so it is not claimed as one. What is still read per call, and is the larger constant: the control block (needed — it is the validation), the header, the log window, and the allocations for all three. Those are what the borrow-based version of this cache is for, and they are the next piece.
This commit is contained in:
@@ -390,6 +390,29 @@ kernel::sync::global_lock! {
|
|||||||
unsafe(uninit) static STORE_FILE: Mutex<Option<StoreFile>> = None;
|
unsafe(uninit) static STORE_FILE: Mutex<Option<StoreFile>> = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The image's space table, held across calls.
|
||||||
|
///
|
||||||
|
/// Every coordinate read binary-searches this table for the space's first record, and every batch of
|
||||||
|
/// a listing reads a row of it — from the device, per probe, for data that changes only when the
|
||||||
|
/// store is folded. Measured, a coordinate read costs the same in a store forty times larger while
|
||||||
|
/// the index search does nine probe reads against fifteen, so the per-call cost is setup rather than
|
||||||
|
/// device reads; a read per probe is the clearest piece of that setup left.
|
||||||
|
///
|
||||||
|
/// Keyed on the **control block's generation**, which the fold raises along with everything else it
|
||||||
|
/// writes, and on the image offset — a fold flips the slot, so both change together and a stale table
|
||||||
|
/// cannot outlive the image it describes. Nothing else has to invalidate it: a writer that bumps the
|
||||||
|
/// generation invalidates it automatically, including a writer this driver never sees.
|
||||||
|
struct TableCache {
|
||||||
|
generation: u64,
|
||||||
|
image_off: u64,
|
||||||
|
table: KVVec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel::sync::global_lock! {
|
||||||
|
/// SAFETY: Initialized (to None) before first use.
|
||||||
|
unsafe(uninit) static TABLE_CACHE: Mutex<Option<TableCache>> = None;
|
||||||
|
}
|
||||||
|
|
||||||
kernel::sync::global_lock! {
|
kernel::sync::global_lock! {
|
||||||
/// Serialises the store's read-modify-write operations.
|
/// Serialises the store's read-modify-write operations.
|
||||||
///
|
///
|
||||||
@@ -2875,6 +2898,8 @@ struct Addressed {
|
|||||||
header: HeaderV3,
|
header: HeaderV3,
|
||||||
/// The log window, which is what can override the image and is small.
|
/// The log window, which is what can override the image and is small.
|
||||||
log: KVVec<u8>,
|
log: KVVec<u8>,
|
||||||
|
/// The image's space table, as it lies — filled from the cache, not from the device.
|
||||||
|
spaces: KVVec<u8>,
|
||||||
/// Reused for every span read, so a lookup does not allocate per probe.
|
/// Reused for every span read, so a lookup does not allocate per probe.
|
||||||
scratch: KVVec<u8>,
|
scratch: KVVec<u8>,
|
||||||
}
|
}
|
||||||
@@ -2895,6 +2920,7 @@ impl Addressed {
|
|||||||
values_off: 0,
|
values_off: 0,
|
||||||
},
|
},
|
||||||
log: KVVec::new(),
|
log: KVVec::new(),
|
||||||
|
spaces: KVVec::new(),
|
||||||
scratch: KVVec::new(),
|
scratch: KVVec::new(),
|
||||||
};
|
};
|
||||||
opened.scratch = KVVec::<u8>::with_capacity(4096, GFP_KERNEL)?;
|
opened.scratch = KVVec::<u8>::with_capacity(4096, GFP_KERNEL)?;
|
||||||
@@ -2918,6 +2944,37 @@ impl Addressed {
|
|||||||
EINVAL
|
EINVAL
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// The space table: the cache's copy when the store has not moved, a fresh read when it has.
|
||||||
|
let bytes = opened.header.space_count as usize * SPACE_ENTRY;
|
||||||
|
if bytes > MAX_BYTES {
|
||||||
|
return Err(EINVAL);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut guard = TABLE_CACHE.lock();
|
||||||
|
if guard.is_none() {
|
||||||
|
*guard = Some(TableCache {
|
||||||
|
generation: u64::MAX,
|
||||||
|
image_off: 0,
|
||||||
|
table: KVVec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let cache = guard.as_mut().unwrap();
|
||||||
|
if cache.generation != control.generation || cache.image_off != opened.image_off {
|
||||||
|
read_exact_at(
|
||||||
|
file,
|
||||||
|
opened.image_off + HEADER_LEN_V3 as u64,
|
||||||
|
bytes,
|
||||||
|
&mut cache.table,
|
||||||
|
&mut opened.scratch,
|
||||||
|
)?;
|
||||||
|
cache.generation = control.generation;
|
||||||
|
cache.image_off = opened.image_off;
|
||||||
|
}
|
||||||
|
// Copied rather than borrowed: the table is `space_count * 48` bytes — a few hundred here,
|
||||||
|
// and the copy is cheaper than the reads it replaces.
|
||||||
|
opened.spaces.extend_from_slice(cache.table.as_slice(), GFP_KERNEL)?;
|
||||||
|
}
|
||||||
|
|
||||||
if control.log_used > 0 {
|
if control.log_used > 0 {
|
||||||
let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES);
|
let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES);
|
||||||
read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?;
|
read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?;
|
||||||
@@ -2946,9 +3003,8 @@ impl Addressed {
|
|||||||
let mut hi = self.header.space_count;
|
let mut hi = self.header.space_count;
|
||||||
while lo < hi {
|
while lo < hi {
|
||||||
let mid = lo + (hi - lo) / 2;
|
let mid = lo + (hi - lo) / 2;
|
||||||
let at = HEADER_LEN_V3 as u64 + mid * SPACE_ENTRY as u64;
|
let from = mid as usize * SPACE_ENTRY;
|
||||||
self.read_image_at(at, SPACE_ENTRY, &mut buf)?;
|
let entry = &self.spaces.as_slice()[from..from + SPACE_ENTRY];
|
||||||
let entry = buf.as_slice();
|
|
||||||
let found = &entry[..SPACE_ID_LEN];
|
let found = &entry[..SPACE_ID_LEN];
|
||||||
if found < &space[..] {
|
if found < &space[..] {
|
||||||
lo = mid + 1;
|
lo = mid + 1;
|
||||||
@@ -2975,9 +3031,8 @@ impl Addressed {
|
|||||||
if row >= self.header.space_count {
|
if row >= self.header.space_count {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut buf = KVVec::<u8>::new();
|
let from = row as usize * SPACE_ENTRY;
|
||||||
self.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf)?;
|
let entry = &self.spaces.as_slice()[from..from + SPACE_ENTRY];
|
||||||
let entry = buf.as_slice();
|
|
||||||
let space: [u8; SPACE_ID_LEN] = match entry[..SPACE_ID_LEN].try_into() {
|
let space: [u8; SPACE_ID_LEN] = match entry[..SPACE_ID_LEN].try_into() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return Err(EINVAL),
|
Err(_) => return Err(EINVAL),
|
||||||
@@ -4478,6 +4533,8 @@ impl kernel::InPlaceModule for CubeStoreModule {
|
|||||||
unsafe { STORE_FILE.init() };
|
unsafe { STORE_FILE.init() };
|
||||||
// SAFETY: called exactly once, in the module initializer, before anything can take it.
|
// SAFETY: called exactly once, in the module initializer, before anything can take it.
|
||||||
unsafe { STORE_OP.init() };
|
unsafe { STORE_OP.init() };
|
||||||
|
// SAFETY: as above.
|
||||||
|
unsafe { TABLE_CACHE.init() };
|
||||||
try_pin_init!(Self {
|
try_pin_init!(Self {
|
||||||
_miscdev <- MiscDeviceRegistration::register(MiscDeviceOptions {
|
_miscdev <- MiscDeviceRegistration::register(MiscDeviceOptions {
|
||||||
name: c_str!("cubelinux"),
|
name: c_str!("cubelinux"),
|
||||||
|
|||||||
Reference in New Issue
Block a user