#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use super::{Block, BlockHandle, GlobalTableId};
use crate::path::Path;
use crate::{
Cache, CompressionType, KeyRange, Table, encryption::EncryptionProvider,
file_accessor::FileAccessor, table::block::BlockType, version::run::Ranged,
};
#[cfg(feature = "metrics")]
use crate::metrics::Metrics;
#[must_use]
pub fn aggregate_run_key_range(tables: &[Table]) -> KeyRange {
#[expect(clippy::expect_used, reason = "runs are never empty by definition")]
let lo = tables.first().expect("run should never be empty");
#[expect(clippy::expect_used, reason = "runs are never empty by definition")]
let hi = tables.last().expect("run should never be empty");
KeyRange::new((lo.key_range().min().clone(), hi.key_range().max().clone()))
}
#[derive(Debug)]
pub struct SliceIndexes(pub usize, pub usize);
pub(crate) trait ReadPacer: Send + Sync {
fn active(&self) -> bool;
fn pace(&self, offset: u64, len: u64);
fn read_took(&self, _elapsed: core::time::Duration) {}
fn read_bytes(&self, _len: u64) {}
}
pub(crate) type Pacer = alloc::sync::Arc<dyn ReadPacer>;
#[cfg(feature = "std")]
type TimedRead = std::io::Result<usize>;
#[cfg(not(feature = "std"))]
type TimedRead = crate::io::Result<usize>;
pub(crate) struct TimedFile {
file: alloc::boxed::Box<dyn crate::fs::FsFile>,
pace: Option<Pacer>,
}
impl TimedFile {
pub(crate) fn new(file: alloc::boxed::Box<dyn crate::fs::FsFile>) -> Self {
Self { file, pace: None }
}
pub(crate) fn set_pace(&mut self, pace: Pacer) {
self.pace = Some(pace);
}
pub(crate) fn into_inner(self) -> alloc::boxed::Box<dyn crate::fs::FsFile> {
self.file
}
fn read_timed(&mut self, buf: &mut [u8]) -> TimedRead {
#[cfg(not(feature = "std"))]
use crate::io::Read;
#[cfg(feature = "std")]
use std::io::Read;
#[cfg(feature = "std")]
let read = if let Some(pace) = self.pace.as_ref().filter(|pace| pace.active()) {
let len = buf.len().min(PACE_PORTION);
let (portion, _) = buf.split_at_mut(len);
let started = std::time::Instant::now();
let read = self.file.read(portion);
pace.read_took(started.elapsed());
read
} else {
self.file.read(buf)
};
#[cfg(not(feature = "std"))]
let read = self.file.read(buf);
if let (Some(pace), Ok(n)) = (&self.pace, &read) {
pace.read_bytes(*n as u64);
}
read
}
}
#[cfg(feature = "std")]
impl std::io::Read for TimedFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_timed(buf)
}
}
#[cfg(not(feature = "std"))]
impl crate::io::Read for TimedFile {
fn read(&mut self, buf: &mut [u8]) -> crate::io::Result<usize> {
self.read_timed(buf)
}
}
#[cfg(feature = "std")]
impl std::io::Seek for TimedFile {
fn seek(&mut self, to: std::io::SeekFrom) -> std::io::Result<u64> {
self.file.seek(to)
}
}
#[cfg(not(feature = "std"))]
impl crate::io::Seek for TimedFile {
fn seek(&mut self, to: crate::io::SeekFrom) -> crate::io::Result<u64> {
self.file.seek(to)
}
}
#[cfg(feature = "std")]
pub(crate) const PACE_PORTION: usize = 64 * 1024;
#[cfg(not(feature = "std"))]
pub(crate) const PACE_PORTION: usize = usize::MAX;
#[derive(Default)]
pub(crate) struct LookupFd {
fd: core::cell::OnceCell<(alloc::sync::Arc<dyn crate::fs::FsFile>, u64)>,
held: Option<HeldFd>,
}
struct HeldFd {
file: alloc::sync::Weak<dyn crate::fs::FsFile>,
epoch: u64,
}
impl LookupFd {
pub(crate) fn resolve(
&self,
file_accessor: &FileAccessor,
table_id: &GlobalTableId,
path: &Path,
charge: ReadCharge,
#[cfg(feature = "metrics")] metrics: &Metrics,
) -> crate::io::Result<&dyn crate::fs::FsFile> {
if let Some((fd, _)) = self.fd.get() {
return Ok(fd.as_ref());
}
let epoch = file_accessor.table_epoch();
if let Some(held) = &self.held
&& held.epoch == epoch
&& let Some(fd) = held.file.upgrade()
{
return Ok(self.fd.get_or_init(|| (fd, epoch)).0.as_ref());
}
let fd = Self::open(
file_accessor,
table_id,
path,
charge,
#[cfg(feature = "metrics")]
metrics,
)?;
Ok(self.fd.get_or_init(|| (fd, epoch)).0.as_ref())
}
pub(crate) fn into_file(
self,
file_accessor: &FileAccessor,
table_id: &GlobalTableId,
path: &Path,
charge: ReadCharge,
#[cfg(feature = "metrics")] metrics: &Metrics,
) -> crate::io::Result<alloc::sync::Arc<dyn crate::fs::FsFile>> {
match self.fd.into_inner() {
Some((fd, _)) => Ok(fd),
None => Self::open(
file_accessor,
table_id,
path,
charge,
#[cfg(feature = "metrics")]
metrics,
),
}
}
fn open(
file_accessor: &FileAccessor,
table_id: &GlobalTableId,
path: &Path,
charge: ReadCharge,
#[cfg(feature = "metrics")] metrics: &Metrics,
) -> crate::io::Result<alloc::sync::Arc<dyn crate::fs::FsFile>> {
let (fd, cache_event) = if charge.touches_cache() {
file_accessor.get_or_open_table(table_id, path)?
} else {
(file_accessor.peek_or_open_table(table_id, path)?, None)
};
#[cfg(feature = "metrics")]
if let Some(hit) = cache_event
&& charge.is_counted()
{
use core::sync::atomic::Ordering::Relaxed;
if hit {
metrics.table_file_opened_cached.fetch_add(1, Relaxed);
} else {
metrics.table_file_opened_uncached.fetch_add(1, Relaxed);
}
}
#[cfg(not(feature = "metrics"))]
let _ = cache_event;
Ok(fd)
}
}
#[derive(Default)]
pub(crate) struct ScanFd(core::cell::Cell<Option<HeldFd>>);
impl ScanFd {
pub(crate) fn lookup(&self) -> LookupFd {
LookupFd {
fd: core::cell::OnceCell::new(),
held: self.0.take(),
}
}
pub(crate) fn keep(&self, read: LookupFd) {
let LookupFd { fd, held } = read;
let kept = match (fd.into_inner(), held) {
(Some((file, epoch)), Some(held))
if held.epoch == epoch
&& core::ptr::addr_eq(held.file.as_ptr(), alloc::sync::Arc::as_ptr(&file)) =>
{
Some(held)
}
(Some((file, epoch)), _) => Some(HeldFd {
file: alloc::sync::Arc::downgrade(&file),
epoch,
}),
(None, held) => held,
};
self.0.set(kept);
}
}
#[expect(
clippy::too_many_arguments,
reason = "block loading requires table id, path, file accessor, cache, handle, block type, compression, and heal context"
)]
pub fn load_block(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
cache: &Cache,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
heal_hints: Option<&crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")] metrics: &Metrics,
charge: ReadCharge,
) -> crate::Result<Block> {
load_block_paced(
table_id,
path,
file_accessor,
cache,
handle,
block_type,
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
heal_hints,
#[cfg(feature = "metrics")]
metrics,
charge,
None,
&LookupFd::default(),
)
}
#[expect(
clippy::too_many_arguments,
reason = "block loading requires table id, path, file accessor, cache, handle, block type, compression, and heal context"
)]
pub(crate) fn load_block_paced(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
cache: &Cache,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
heal_hints: Option<&crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")] metrics: &Metrics,
charge: ReadCharge,
pace: Option<&dyn ReadPacer>,
fd: &LookupFd,
) -> crate::Result<Block> {
log::trace!("load {block_type:?} block {handle:?}");
if matches!(block_type, BlockType::Manifest | BlockType::ManifestFooter) {
return Err(crate::Error::InvalidTag(("BlockType", block_type.into())));
}
let cached = if charge.touches_cache() {
cache.get_block(table_id, handle.offset())
} else {
cache.peek_block(table_id, handle.offset())
};
if let Some(block) = cached {
if block.header.block_type != block_type {
return Err(crate::Error::InvalidTag((
"BlockType",
block.header.block_type.into(),
)));
}
#[cfg(feature = "metrics")]
if charge.is_counted() {
record_block_load_cached(metrics, block_type);
}
return Ok(block);
}
let coordinated = charge.touches_cache() && pace.is_none();
let lead = if coordinated {
match cache.claim_block(
table_id,
handle.offset(),
crate::cache::LoadIdentity::Block {
block_type,
size: handle.size(),
compression,
encrypted: encryption.is_some(),
},
) {
crate::cache::BlockClaim::Shared(block) => {
#[cfg(feature = "metrics")]
if charge.is_counted() {
metrics
.block_load_shared
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
if !cache.has_block(table_id, handle.offset()) {
cache.insert_block(table_id, handle.offset(), block.clone());
}
return Ok(block);
}
crate::cache::BlockClaim::Lead(lead) => {
if let Some(block) = cache.get_block(table_id, handle.offset())
&& block.header.block_type == block_type
{
#[cfg(feature = "metrics")]
if charge.is_counted() {
record_block_load_cached(metrics, block_type);
}
lead.publish(&block);
return Ok(block);
}
Some(lead)
}
crate::cache::BlockClaim::Alone => None,
}
} else {
None
};
let timer = crate::cache::Cache::time_load();
let fd = fd.resolve(
file_accessor,
&table_id,
path,
charge,
#[cfg(feature = "metrics")]
metrics,
)?;
let transform = build_block_transform(
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
)?;
let mut produced = 0;
let read = Block::from_file_issuing_paced(
fd,
*handle,
crate::table::block::BlockIdentity {
table_id: table_id.table_id(),
block_type,
dict_id: compression.dict_id(),
window_log: 0,
},
&transform,
|| {
#[cfg(feature = "metrics")]
if charge.is_counted() {
record_block_read(metrics, block_type, handle.size().into());
}
},
pace,
&mut produced,
);
#[cfg(feature = "metrics")]
record_block_decoded(metrics, charge, produced);
let (block, ecc_status, recovery) = read?;
let block = admit_read_block(
table_id,
path,
file_accessor,
handle,
block_type,
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
heal_hints,
#[cfg(feature = "metrics")]
metrics,
charge,
block,
ecc_status,
recovery,
)?;
if coordinated {
cache.record_block_load(table_id, handle.offset(), timer);
}
if charge.touches_cache() {
cache.insert_block(table_id, handle.offset(), block.clone());
}
if let Some(lead) = lead {
lead.publish(&block);
}
Ok(block)
}
#[expect(
clippy::too_many_arguments,
reason = "the whole block read context, plus the block it admits"
)]
pub(crate) fn admit_read_block(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
heal_hints: Option<&crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")] metrics: &Metrics,
charge: ReadCharge,
block: Block,
ecc_status: crate::table::block::EccStatus,
recovery: Option<crate::table::block::EccRecoveryKind>,
) -> crate::Result<Block> {
#[cfg(feature = "metrics")]
if let Some(kind) = recovery {
metrics.record_ecc_recovery(kind);
}
#[cfg(not(feature = "metrics"))]
let _ = recovery;
let corrected = matches!(ecc_status, crate::table::block::EccStatus::Corrected);
if block.header.block_type != block_type {
return Err(crate::Error::InvalidTag((
"BlockType",
block.header.block_type.into(),
)));
}
#[cfg(feature = "metrics")]
if charge.is_counted() {
record_block_loaded(metrics, block_type);
}
if corrected {
maybe_record_persistent_heal(
table_id,
path,
file_accessor,
handle,
block_type,
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
heal_hints,
#[cfg(feature = "metrics")]
metrics,
charge,
);
}
Ok(block)
}
#[cfg(feature = "metrics")]
pub(crate) fn record_block_load_cached(metrics: &Metrics, block_type: BlockType) {
use core::sync::atomic::Ordering::Relaxed;
let cached = match block_type {
BlockType::Filter => &metrics.filter_block_load_cached,
BlockType::Index | BlockType::ColumnPageDirectory | BlockType::ColumnZones => {
&metrics.index_block_load_cached
}
BlockType::RangeTombstone => &metrics.range_tombstone_block_load_cached,
BlockType::Data | BlockType::Meta | BlockType::ColumnPage => {
&metrics.data_block_load_cached
}
BlockType::Manifest
| BlockType::ManifestFooter
| BlockType::BlockLayout
| BlockType::Locator
| BlockType::SeqnoBounds
| BlockType::ZoneMap
| BlockType::DeleteBitmap
| BlockType::OwnedBlobObjects => return,
};
cached.fetch_add(1, Relaxed);
}
#[cfg(feature = "metrics")]
pub(crate) fn record_block_read(metrics: &Metrics, block_type: BlockType, on_disk: u64) {
use core::sync::atomic::Ordering::Relaxed;
let requested = match block_type {
BlockType::Filter => &metrics.filter_block_io_requested,
BlockType::Index | BlockType::ColumnPageDirectory | BlockType::ColumnZones => {
&metrics.index_block_io_requested
}
BlockType::RangeTombstone => &metrics.range_tombstone_block_io_requested,
BlockType::Data | BlockType::Meta | BlockType::ColumnPage => {
&metrics.data_block_io_requested
}
BlockType::Manifest
| BlockType::ManifestFooter
| BlockType::BlockLayout
| BlockType::Locator
| BlockType::SeqnoBounds
| BlockType::ZoneMap
| BlockType::DeleteBitmap
| BlockType::OwnedBlobObjects => return,
};
requested.fetch_add(on_disk, Relaxed);
}
#[cfg(feature = "metrics")]
pub(crate) fn record_block_loaded(metrics: &Metrics, block_type: BlockType) {
use core::sync::atomic::Ordering::Relaxed;
let loads = match block_type {
BlockType::Filter => &metrics.filter_block_load_io,
BlockType::Index | BlockType::ColumnPageDirectory | BlockType::ColumnZones => {
&metrics.index_block_load_io
}
BlockType::RangeTombstone => &metrics.range_tombstone_block_load_io,
BlockType::Data | BlockType::Meta | BlockType::ColumnPage => &metrics.data_block_load_io,
BlockType::Manifest
| BlockType::ManifestFooter
| BlockType::BlockLayout
| BlockType::Locator
| BlockType::SeqnoBounds
| BlockType::ZoneMap
| BlockType::DeleteBitmap
| BlockType::OwnedBlobObjects => return,
};
loads.fetch_add(1, Relaxed);
}
#[cfg(feature = "metrics")]
pub(crate) fn record_block_decoded(metrics: &Metrics, charge: ReadCharge, produced: usize) {
if charge.is_counted() {
metrics
.block_bytes_decoded
.fetch_add(produced as u64, core::sync::atomic::Ordering::Relaxed);
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors the block read context needed for the confirming re-read"
)]
pub(crate) fn maybe_record_persistent_heal(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
heal_hints: Option<&crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")] metrics: &Metrics,
charge: ReadCharge,
) -> bool {
let Some(hints) = heal_hints else {
return false;
};
if !hints.is_enabled() {
return false;
}
let mut decoded = 0;
let reread = reread_block_is_corrected(
table_id,
path,
file_accessor,
handle,
block_type,
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
#[cfg(feature = "metrics")]
metrics,
charge,
&mut decoded,
);
#[cfg(feature = "metrics")]
if charge.is_counted() {
metrics
.block_bytes_decoded
.fetch_add(decoded as u64, core::sync::atomic::Ordering::Relaxed);
}
match reread {
Ok(corrected) => {
if !corrected {
log::debug!(
"Transient ECC correction on table {table_id:?} block {handle:?}; \
re-read clean, not scheduling"
);
return false;
}
if hints.record(table_id) {
#[cfg(feature = "metrics")]
metrics
.ecc_auto_heal_scheduled
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
log::warn!(
"Persistent ECC correction on table {table_id:?} block {handle:?}; \
queued for healing recompaction"
);
return true;
}
false
}
Err(e) => {
log::debug!(
"ECC re-read confirmation for table {table_id:?} block {handle:?} failed: {e:?}"
);
false
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ReadCharge {
Foreground,
Maintenance,
Untraced,
}
impl ReadCharge {
#[cfg(feature = "metrics")]
#[must_use]
pub(crate) const fn is_counted(self) -> bool {
matches!(self, Self::Foreground)
}
#[must_use]
pub(crate) const fn touches_cache(self) -> bool {
!matches!(self, Self::Untraced)
}
}
#[cfg(feature = "std")]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum BlockScrubOutcome {
Clean,
Corrected {
scheduled: bool,
},
}
#[cfg(feature = "std")]
#[expect(
clippy::too_many_arguments,
reason = "mirrors load_block's read context minus the cache"
)]
pub(crate) fn scrub_block(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
heal_hints: Option<&crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")] metrics: &Metrics,
) -> crate::Result<BlockScrubOutcome> {
let fd = file_accessor.peek_or_open_table(&table_id, path)?;
let transform = build_block_transform(
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
)?;
let (block, ecc_status, recovery) = Block::from_file_with_recovery(
fd.as_ref(),
*handle,
crate::table::block::BlockIdentity {
table_id: table_id.table_id(),
block_type,
dict_id: compression.dict_id(),
window_log: 0,
},
&transform,
)?;
if block.header.block_type != block_type {
return Err(crate::Error::InvalidTag((
"BlockType",
block.header.block_type.into(),
)));
}
Ok(match ecc_status {
crate::table::block::EccStatus::Corrected => {
#[cfg(feature = "metrics")]
if let Some(kind) = recovery {
metrics.record_ecc_recovery(kind);
}
#[cfg(not(feature = "metrics"))]
let _ = recovery;
let scheduled = maybe_record_persistent_heal(
table_id,
path,
file_accessor,
handle,
block_type,
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
heal_hints,
#[cfg(feature = "metrics")]
metrics,
ReadCharge::Untraced,
);
BlockScrubOutcome::Corrected { scheduled }
}
crate::table::block::EccStatus::Ok | crate::table::block::EccStatus::Unrecognized => {
BlockScrubOutcome::Clean
}
})
}
#[cfg_attr(
not(zstd_any),
expect(
clippy::elidable_lifetime_names,
reason = "named to stay valid in the zstd build, where a second reference parameter shares it"
)
)]
pub(crate) fn build_block_transform<'a>(
compression: CompressionType,
encryption: Option<&'a dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&'a crate::compression::ZstdDictionary>,
) -> crate::Result<crate::table::block::BlockTransform<'a>> {
let t = crate::table::block::BlockTransform::from_parts(
compression,
encryption,
#[cfg(zstd_any)]
zstd_dict,
)?;
Ok(if let Some(ecc) = ecc {
t.with_ecc(ecc)
} else {
t
})
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors load_block's read context minus the cache"
)]
fn reread_block_is_corrected(
table_id: GlobalTableId,
path: &Path,
file_accessor: &FileAccessor,
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
encryption: Option<&dyn EncryptionProvider>,
ecc: Option<crate::table::block::EccParams>,
#[cfg(zstd_any)] zstd_dict: Option<&crate::compression::ZstdDictionary>,
#[cfg(feature = "metrics")] metrics: &Metrics,
charge: ReadCharge,
decoded: &mut usize,
) -> crate::Result<bool> {
let fd = if charge.touches_cache() {
file_accessor.get_or_open_table(&table_id, path)?.0
} else {
file_accessor.peek_or_open_table(&table_id, path)?
};
let transform = build_block_transform(
compression,
encryption,
ecc,
#[cfg(zstd_any)]
zstd_dict,
)?;
let (_block, ecc_status, _recovery) = Block::from_file_issuing(
fd.as_ref(),
*handle,
crate::table::block::BlockIdentity {
table_id: table_id.table_id(),
block_type,
dict_id: compression.dict_id(),
window_log: 0,
},
&transform,
|| {
#[cfg(feature = "metrics")]
if charge.is_counted() {
record_block_read(metrics, block_type, handle.size().into());
}
},
decoded,
)?;
Ok(matches!(
ecc_status,
crate::table::block::EccStatus::Corrected
))
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
cpufeatures::new!(cpu_avx512bw, "avx512bw");
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
cpufeatures::new!(cpu_avx2, "avx2");
#[cfg(target_arch = "x86")]
cpufeatures::new!(cpu_sse2, "sse2");
#[must_use]
pub fn longest_shared_prefix_length(s1: &[u8], s2: &[u8]) -> usize {
unsafe { resolve_lsp_kernel()(s1, s2) }
}
pub type LspKernel = unsafe fn(&[u8], &[u8]) -> usize;
#[must_use]
pub fn resolve_lsp_kernel() -> LspKernel {
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{
select_lsp_kernel(LspCpuFeatures {
avx512bw: cpu_avx512bw::get(),
avx2: cpu_avx2::get(),
#[cfg(target_arch = "x86_64")]
sse2: true,
#[cfg(target_arch = "x86")]
sse2: cpu_sse2::get(),
})
}
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
{
lsp_neon
}
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "x86",
all(target_arch = "aarch64", target_endian = "little")
)))]
{
lsp_scalar
}
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[derive(Clone, Copy)]
struct LspCpuFeatures {
avx512bw: bool,
avx2: bool,
sse2: bool,
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
fn select_lsp_kernel(f: LspCpuFeatures) -> LspKernel {
if f.avx512bw {
lsp_avx512
} else if f.avx2 {
lsp_avx2
} else if f.sse2 {
lsp_sse2
} else {
lsp_scalar
}
}
#[cfg_attr(
all(target_arch = "aarch64", target_endian = "little", not(test)),
expect(
dead_code,
reason = "portable LSP fallback; aarch64-LE always selects the NEON kernel \
(unit tests still call it directly, hence not(test))"
)
)]
#[must_use]
pub(crate) fn lsp_scalar(s1: &[u8], s2: &[u8]) -> usize {
let min_len = s1.len().min(s2.len());
let mut i = 0;
while i + 8 <= min_len {
#[expect(unsafe_code, reason = "bounds checked by loop guard above")]
let (a, b) = unsafe {
(
s1.as_ptr().add(i).cast::<u64>().read_unaligned(),
s2.as_ptr().add(i).cast::<u64>().read_unaligned(),
)
};
let diff = a ^ b;
if diff != 0 {
#[cfg(target_endian = "little")]
let byte_off = (diff.trailing_zeros() / 8) as usize;
#[cfg(target_endian = "big")]
let byte_off = (diff.leading_zeros() / 8) as usize;
return i + byte_off;
}
i += 8;
}
while i < min_len {
#[expect(unsafe_code, reason = "i < min_len bounds-checked above")]
let (a, b) = unsafe { (*s1.get_unchecked(i), *s2.get_unchecked(i)) };
if a != b {
return i;
}
i += 1;
}
min_len
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "avx2")]
#[expect(unsafe_code, reason = "intrinsics require unsafe")]
#[must_use]
unsafe fn lsp_avx2(s1: &[u8], s2: &[u8]) -> usize {
#[cfg(target_arch = "x86")]
use core::arch::x86::{__m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8};
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::{
__m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8,
};
let min_len = s1.len().min(s2.len());
let mut i = 0;
while i + 32 <= min_len {
#[expect(
clippy::cast_ptr_alignment,
reason = "_mm256_loadu_si256 explicitly performs an unaligned 32-byte load"
)]
let (va, vb) = unsafe {
(
_mm256_loadu_si256(s1.as_ptr().add(i).cast::<__m256i>()),
_mm256_loadu_si256(s2.as_ptr().add(i).cast::<__m256i>()),
)
};
let cmp = _mm256_cmpeq_epi8(va, vb);
let mask = _mm256_movemask_epi8(cmp).cast_unsigned();
if mask != u32::MAX {
return i + (!mask).trailing_zeros() as usize;
}
i += 32;
}
while i < min_len {
let (a, b) = unsafe { (*s1.get_unchecked(i), *s2.get_unchecked(i)) };
if a != b {
return i;
}
i += 1;
}
min_len
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "avx512bw,avx512f")]
#[expect(unsafe_code, reason = "intrinsics require unsafe")]
#[must_use]
unsafe fn lsp_avx512(s1: &[u8], s2: &[u8]) -> usize {
#[cfg(target_arch = "x86")]
use core::arch::x86::{__m512i, _mm512_cmpeq_epi8_mask, _mm512_loadu_si512};
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::{__m512i, _mm512_cmpeq_epi8_mask, _mm512_loadu_si512};
let min_len = s1.len().min(s2.len());
let mut i = 0;
while i + 64 <= min_len {
#[expect(
clippy::cast_ptr_alignment,
reason = "_mm512_loadu_si512 explicitly performs an unaligned 64-byte load"
)]
let (va, vb) = unsafe {
(
_mm512_loadu_si512(s1.as_ptr().add(i).cast::<__m512i>()),
_mm512_loadu_si512(s2.as_ptr().add(i).cast::<__m512i>()),
)
};
let mask = _mm512_cmpeq_epi8_mask(va, vb);
if mask != u64::MAX {
return i + (!mask).trailing_zeros() as usize;
}
i += 64;
}
while i < min_len {
let (a, b) = unsafe { (*s1.get_unchecked(i), *s2.get_unchecked(i)) };
if a != b {
return i;
}
i += 1;
}
min_len
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "sse2")]
#[expect(unsafe_code, reason = "intrinsics require unsafe")]
#[must_use]
unsafe fn lsp_sse2(s1: &[u8], s2: &[u8]) -> usize {
#[cfg(target_arch = "x86")]
use core::arch::x86::{__m128i, _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8};
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::{__m128i, _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8};
let min_len = s1.len().min(s2.len());
let mut i = 0;
while i + 16 <= min_len {
#[expect(
clippy::cast_ptr_alignment,
reason = "_mm_loadu_si128 explicitly performs an unaligned 16-byte load"
)]
let (va, vb) = unsafe {
(
_mm_loadu_si128(s1.as_ptr().add(i).cast::<__m128i>()),
_mm_loadu_si128(s2.as_ptr().add(i).cast::<__m128i>()),
)
};
let cmp = _mm_cmpeq_epi8(va, vb);
let mask = _mm_movemask_epi8(cmp).cast_unsigned();
if mask != 0xFFFF {
return i + (!mask).trailing_zeros() as usize;
}
i += 16;
}
while i < min_len {
let (a, b) = unsafe { (*s1.get_unchecked(i), *s2.get_unchecked(i)) };
if a != b {
return i;
}
i += 1;
}
min_len
}
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
#[target_feature(enable = "neon")]
#[expect(unsafe_code, reason = "intrinsics require unsafe")]
#[must_use]
unsafe fn lsp_neon(s1: &[u8], s2: &[u8]) -> usize {
use core::arch::aarch64::{
vandq_u8, vceqq_u8, vdupq_n_u8, vgetq_lane_u64, vld1q_u8, vreinterpretq_u64_u8,
};
let min_len = s1.len().min(s2.len());
let mut i = 0;
while i + 16 <= min_len {
let (va, vb) = unsafe { (vld1q_u8(s1.as_ptr().add(i)), vld1q_u8(s2.as_ptr().add(i))) };
let cmp = vceqq_u8(va, vb);
let masked = vandq_u8(cmp, vdupq_n_u8(0xFF));
let as_u64 = vreinterpretq_u64_u8(masked);
let lo = vgetq_lane_u64(as_u64, 0);
let hi = vgetq_lane_u64(as_u64, 1);
if lo != u64::MAX {
return i + (!lo).trailing_zeros() as usize / 8;
}
if hi != u64::MAX {
return i + 8 + (!hi).trailing_zeros() as usize / 8;
}
i += 16;
}
while i < min_len {
let (a, b) = unsafe { (*s1.get_unchecked(i), *s2.get_unchecked(i)) };
if a != b {
return i;
}
i += 1;
}
min_len
}
#[must_use]
pub fn compare_prefixed_slice(
prefix: &[u8],
suffix: &[u8],
needle: &[u8],
cmp: &dyn crate::comparator::UserComparator,
) -> core::cmp::Ordering {
if cmp.is_lexicographic() {
return compare_prefixed_slice_lexicographic(prefix, suffix, needle);
}
let total_len = prefix.len() + suffix.len();
if total_len <= 256 {
let mut buf = [0_u8; 256];
#[expect(clippy::indexing_slicing, reason = "total_len <= 256 checked above")]
{
buf[..prefix.len()].copy_from_slice(prefix);
buf[prefix.len()..total_len].copy_from_slice(suffix);
}
#[expect(clippy::indexing_slicing, reason = "total_len <= 256 checked above")]
return cmp.compare(&buf[..total_len], needle);
}
let mut full_key = Vec::with_capacity(total_len);
full_key.extend_from_slice(prefix);
full_key.extend_from_slice(suffix);
cmp.compare(&full_key, needle)
}
#[must_use]
fn compare_prefixed_slice_lexicographic(
prefix: &[u8],
suffix: &[u8],
needle: &[u8],
) -> core::cmp::Ordering {
use core::cmp::Ordering::{Equal, Greater};
if needle.is_empty() {
let combined_len = prefix.len() + suffix.len();
return if combined_len > 0 { Greater } else { Equal };
}
let max_pfx_len = prefix.len().min(needle.len());
{
#[expect(
unsafe_code,
reason = "max_pfx_len <= prefix.len() && max_pfx_len <= needle.len()"
)]
let pfx = unsafe { prefix.get_unchecked(0..max_pfx_len) };
#[expect(
unsafe_code,
reason = "max_pfx_len <= prefix.len() && max_pfx_len <= needle.len()"
)]
let ndl = unsafe { needle.get_unchecked(0..max_pfx_len) };
match pfx.cmp(ndl) {
Equal => {}
ordering => return ordering,
}
}
let rest_len = prefix.len().saturating_sub(needle.len());
if rest_len > 0 {
return Greater;
}
#[expect(
unsafe_code,
reason = "max_pfx_len <= needle.len() guaranteed by rest_len == 0 guard above"
)]
let remaining_needle = unsafe { needle.get_unchecked(max_pfx_len..) };
suffix.cmp(remaining_needle)
}
#[cfg(test)]
mod tests;