Skip to main content

rustyhdf5_format/
chunk_cache.rs

1//! Chunk cache with hash-based index and LRU eviction.
2//!
3//! The [`ChunkCache`] avoids re-traversing B-trees on repeated reads of chunked
4//! datasets.  On first access it scans the B-tree once and builds a
5//! `HashMap<ChunkCoord, ChunkInfo>` (the *chunk index*).  Decompressed chunk
6//! data is cached with LRU eviction controlled by a byte-budget.
7
8#[cfg(not(feature = "std"))]
9extern crate alloc;
10
11#[cfg(not(feature = "std"))]
12use alloc::{vec, vec::Vec};
13
14use std::alloc;
15use std::sync::Mutex;
16use core::ops::{Deref, DerefMut};
17
18#[cfg(feature = "std")]
19use std::collections::HashMap;
20#[cfg(not(feature = "std"))]
21use alloc::collections::BTreeMap;
22
23use crate::chunked_read::ChunkInfo;
24
25// ---------------------------------------------------------------------------
26// Cache-line alignment constants (TVL — Tensor Virtualization Layout)
27// ---------------------------------------------------------------------------
28
29/// Cache line size in bytes for the target architecture.
30///
31/// ARM64 uses 128-byte cache lines; x86_64 uses 64-byte. We align all chunk
32/// buffers to this boundary so SIMD operations can assume aligned input.
33#[cfg(target_arch = "aarch64")]
34pub const CACHE_LINE_SIZE: usize = 128;
35
36#[cfg(target_arch = "x86_64")]
37pub const CACHE_LINE_SIZE: usize = 64;
38
39#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
40pub const CACHE_LINE_SIZE: usize = 64;
41
42/// Round `size` up to the next multiple of [`CACHE_LINE_SIZE`].
43#[inline]
44pub fn align_to_cache_line(size: usize) -> usize {
45    (size + CACHE_LINE_SIZE - 1) & !(CACHE_LINE_SIZE - 1)
46}
47
48// ---------------------------------------------------------------------------
49// CacheAlignedBuffer
50// ---------------------------------------------------------------------------
51
52/// A byte buffer whose data pointer is aligned to [`CACHE_LINE_SIZE`].
53///
54/// This enables SIMD operations to use aligned loads/stores when processing
55/// chunk data, avoiding the penalty of misaligned memory accesses.
56///
57/// The buffer is backed by `std::alloc::Layout`-controlled allocation. It
58/// dereferences to `&[u8]` / `&mut [u8]` for seamless use.
59pub struct CacheAlignedBuffer {
60    ptr: *mut u8,
61    len: usize,
62    capacity: usize,
63}
64
65// SAFETY: The raw pointer is exclusively owned — no aliasing.
66unsafe impl Send for CacheAlignedBuffer {}
67unsafe impl Sync for CacheAlignedBuffer {}
68
69impl CacheAlignedBuffer {
70    /// Allocate a new cache-line-aligned buffer of exactly `len` bytes,
71    /// initialized to zero.
72    pub fn zeroed(len: usize) -> Self {
73        if len == 0 {
74            return Self {
75                ptr: core::ptr::NonNull::dangling().as_ptr(),
76                len: 0,
77                capacity: 0,
78            };
79        }
80        let capacity = align_to_cache_line(len);
81        let layout = core::alloc::Layout::from_size_align(capacity, CACHE_LINE_SIZE)
82            .expect("invalid layout");
83        // SAFETY: layout has non-zero size.
84        let ptr = unsafe { alloc::alloc_zeroed(layout) };
85        if ptr.is_null() {
86            alloc::handle_alloc_error(layout);
87        }
88        Self { ptr, len, capacity }
89    }
90
91    /// Create a cache-line-aligned copy of an existing byte slice.
92    pub fn from_slice(data: &[u8]) -> Self {
93        let mut buf = Self::zeroed(data.len());
94        buf.as_mut_slice()[..data.len()].copy_from_slice(data);
95        buf
96    }
97
98    /// Create from an existing `Vec<u8>`, copying into an aligned allocation.
99    pub fn from_vec(v: Vec<u8>) -> Self {
100        Self::from_slice(&v)
101    }
102
103    /// The length of the valid data (may be less than capacity).
104    #[inline]
105    pub fn len(&self) -> usize {
106        self.len
107    }
108
109    /// Whether the buffer is empty.
110    #[inline]
111    pub fn is_empty(&self) -> bool {
112        self.len == 0
113    }
114
115    /// The underlying aligned pointer.
116    #[inline]
117    pub fn as_ptr(&self) -> *const u8 {
118        self.ptr
119    }
120
121    /// Mutable pointer to the data.
122    #[inline]
123    pub fn as_mut_ptr(&mut self) -> *mut u8 {
124        self.ptr
125    }
126
127    /// Borrow as a byte slice.
128    #[inline]
129    pub fn as_slice(&self) -> &[u8] {
130        if self.len == 0 {
131            return &[];
132        }
133        // SAFETY: ptr is valid for `len` bytes and properly aligned.
134        unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
135    }
136
137    /// Borrow as a mutable byte slice.
138    #[inline]
139    pub fn as_mut_slice(&mut self) -> &mut [u8] {
140        if self.len == 0 {
141            return &mut [];
142        }
143        // SAFETY: ptr is valid for `len` bytes and properly aligned.
144        unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) }
145    }
146
147    /// Convert to a `Vec<u8>` (copies data into a standard allocation).
148    pub fn to_vec(&self) -> Vec<u8> {
149        self.as_slice().to_vec()
150    }
151
152    /// Returns `true` if the data pointer is aligned to `CACHE_LINE_SIZE`.
153    #[inline]
154    pub fn is_aligned(&self) -> bool {
155        self.len == 0 || (self.ptr as usize) % CACHE_LINE_SIZE == 0
156    }
157}
158
159impl Drop for CacheAlignedBuffer {
160    fn drop(&mut self) {
161        if self.capacity > 0 {
162            let layout = core::alloc::Layout::from_size_align(self.capacity, CACHE_LINE_SIZE)
163                .expect("invalid layout");
164            // SAFETY: ptr was allocated with this layout.
165            unsafe { alloc::dealloc(self.ptr, layout) };
166        }
167    }
168}
169
170impl Clone for CacheAlignedBuffer {
171    fn clone(&self) -> Self {
172        Self::from_slice(self.as_slice())
173    }
174}
175
176impl Deref for CacheAlignedBuffer {
177    type Target = [u8];
178    #[inline]
179    fn deref(&self) -> &[u8] {
180        self.as_slice()
181    }
182}
183
184impl DerefMut for CacheAlignedBuffer {
185    #[inline]
186    fn deref_mut(&mut self) -> &mut [u8] {
187        self.as_mut_slice()
188    }
189}
190
191impl core::fmt::Debug for CacheAlignedBuffer {
192    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
193        f.debug_struct("CacheAlignedBuffer")
194            .field("len", &self.len)
195            .field("capacity", &self.capacity)
196            .field("aligned", &self.is_aligned())
197            .finish()
198    }
199}
200
201/// Coordinate key for a chunk — the N-dimensional offset vector.
202pub type ChunkCoord = Vec<u64>;
203
204/// Default maximum bytes of decompressed chunk data to cache.
205pub const DEFAULT_CACHE_BYTES: usize = 1024 * 1024; // 1 MiB
206
207/// Default maximum number of cached decompressed chunks.
208pub const DEFAULT_MAX_SLOTS: usize = 16;
209
210// ---------------------------------------------------------------------------
211// LRU entry
212// ---------------------------------------------------------------------------
213
214struct CachedChunk {
215    coord: ChunkCoord,
216    data: CacheAlignedBuffer,
217    /// Monotonically increasing access counter for LRU ordering.
218    last_access: u64,
219}
220
221// ---------------------------------------------------------------------------
222// ChunkCache
223// ---------------------------------------------------------------------------
224
225/// A per-dataset chunk cache with hash-based index and LRU eviction.
226///
227/// # Usage
228///
229/// ```ignore
230/// let cache = ChunkCache::new();
231/// // Pass &cache to read_chunked_data — it will populate the index lazily.
232/// ```
233///
234/// The cache is wrapped in `Mutex` internally so it can be mutated through
235/// shared references (thread-safe).
236pub struct ChunkCache {
237    inner: Mutex<CacheInner>,
238}
239
240struct CacheInner {
241    /// Hash index: chunk coordinate → ChunkInfo (offset + size in file).
242    /// Populated once per dataset on first access.
243    #[cfg(feature = "std")]
244    index: Option<HashMap<ChunkCoord, ChunkInfo>>,
245    #[cfg(not(feature = "std"))]
246    index: Option<BTreeMap<ChunkCoord, ChunkInfo>>,
247
248    /// LRU cache of decompressed chunk data.
249    slots: Vec<CachedChunk>,
250
251    /// Current total bytes of cached decompressed data.
252    current_bytes: usize,
253
254    /// Maximum bytes of decompressed data to cache.
255    max_bytes: usize,
256
257    /// Maximum number of slots.
258    max_slots: usize,
259
260    /// Monotonic counter for LRU ordering.
261    tick: u64,
262
263    /// Last accessed chunk coordinate (for sequential detection).
264    last_coord: Option<ChunkCoord>,
265
266    /// Access pattern statistics.
267    stats: AccessStats,
268}
269
270/// Access pattern statistics tracked by the chunk cache.
271///
272/// Updated on each `get_decompressed` / `put_decompressed` call to help
273/// the sweep detector understand the workload.
274#[derive(Debug, Clone, Default)]
275pub struct AccessStats {
276    /// Number of accesses that followed a sequential pattern.
277    pub sequential_count: u64,
278    /// Number of accesses that appeared random (non-sequential).
279    pub random_count: u64,
280    /// Last detected sweep direction description (informational).
281    pub sweep_direction: Option<&'static str>,
282}
283
284impl ChunkCache {
285    /// Create a new chunk cache with default limits (1 MiB, 16 slots).
286    pub fn new() -> Self {
287        Self::with_capacity(DEFAULT_CACHE_BYTES, DEFAULT_MAX_SLOTS)
288    }
289
290    /// Create a new chunk cache with custom byte budget and slot count.
291    pub fn with_capacity(max_bytes: usize, max_slots: usize) -> Self {
292        Self {
293            inner: Mutex::new(CacheInner {
294                index: None,
295                slots: Vec::with_capacity(max_slots.min(64)),
296                current_bytes: 0,
297                max_bytes,
298                max_slots,
299                tick: 0,
300                last_coord: None,
301                stats: AccessStats::default(),
302            }),
303        }
304    }
305
306    // ----- Index operations -----
307
308    /// Returns `true` if the chunk index has been built.
309    pub fn has_index(&self) -> bool {
310        self.inner.lock().unwrap().index.is_some()
311    }
312
313    /// Build the chunk index from a pre-collected list of `ChunkInfo`.
314    ///
315    /// The `rank` parameter is used to truncate offsets to spatial dims only
316    /// (B-tree v1 stores rank+1 offsets).
317    pub fn populate_index(&self, chunks: &[ChunkInfo], rank: usize) {
318        let mut inner = self.inner.lock().unwrap();
319        if inner.index.is_some() {
320            return; // already populated
321        }
322        #[cfg(feature = "std")]
323        let mut map = HashMap::with_capacity(chunks.len());
324        #[cfg(not(feature = "std"))]
325        let mut map = BTreeMap::new();
326
327        for ci in chunks {
328            let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
329            map.insert(coord, ci.clone());
330        }
331        inner.index = Some(map);
332    }
333
334    /// Look up a chunk by its spatial coordinate in the index.
335    pub fn lookup_index(&self, coord: &[u64]) -> Option<ChunkInfo> {
336        let inner = self.inner.lock().unwrap();
337        inner.index.as_ref()?.get(coord).cloned()
338    }
339
340    /// Return all indexed chunks as a `Vec<ChunkInfo>` (order unspecified).
341    pub fn all_indexed_chunks(&self) -> Option<Vec<ChunkInfo>> {
342        let inner = self.inner.lock().unwrap();
343        inner.index.as_ref().map(|m| m.values().cloned().collect())
344    }
345
346    // ----- Decompressed data cache (LRU) -----
347
348    /// Try to get cached decompressed data for a chunk coordinate.
349    ///
350    /// Returns a clone of the cache-line-aligned buffer.
351    pub fn get_decompressed(&self, coord: &[u64]) -> Option<Vec<u8>> {
352        let mut inner = self.inner.lock().unwrap();
353        inner.tick += 1;
354        let tick = inner.tick;
355
356        // Track sequential vs random access
357        let is_sequential = inner.last_coord.as_ref().map_or(false, |prev| {
358            // Sequential if exactly one dimension changed
359            let changes: usize = prev.iter().zip(coord.iter())
360                .filter(|(a, b)| a != b)
361                .count();
362            changes <= 1
363        });
364        if is_sequential {
365            inner.stats.sequential_count += 1;
366        } else if inner.last_coord.is_some() {
367            inner.stats.random_count += 1;
368        }
369        inner.last_coord = Some(coord.to_vec());
370
371        for slot in inner.slots.iter_mut() {
372            if slot.coord.as_slice() == coord {
373                slot.last_access = tick;
374                return Some(slot.data.to_vec());
375            }
376        }
377        None
378    }
379
380    /// Try to get a reference-counted clone of the aligned buffer for a chunk.
381    pub fn get_decompressed_aligned(&self, coord: &[u64]) -> Option<CacheAlignedBuffer> {
382        let mut inner = self.inner.lock().unwrap();
383        inner.tick += 1;
384        let tick = inner.tick;
385        for slot in inner.slots.iter_mut() {
386            if slot.coord.as_slice() == coord {
387                slot.last_access = tick;
388                return Some(slot.data.clone());
389            }
390        }
391        None
392    }
393
394    /// Insert decompressed chunk data into the LRU cache.
395    ///
396    /// The data is stored in a [`CacheAlignedBuffer`] so subsequent reads
397    /// return cache-line-aligned memory.
398    pub fn put_decompressed(&self, coord: ChunkCoord, data: Vec<u8>) {
399        let aligned = CacheAlignedBuffer::from_slice(&data);
400        self.put_decompressed_aligned(coord, aligned);
401    }
402
403    /// Insert an already-aligned buffer into the LRU cache.
404    pub fn put_decompressed_aligned(&self, coord: ChunkCoord, data: CacheAlignedBuffer) {
405        let mut inner = self.inner.lock().unwrap();
406        let data_len = data.len();
407
408        // Don't cache if single chunk exceeds budget
409        if data_len > inner.max_bytes {
410            return;
411        }
412
413        // Check if already present
414        inner.tick += 1;
415        let tick = inner.tick;
416        for slot in inner.slots.iter_mut() {
417            if slot.coord == coord {
418                slot.last_access = tick;
419                return; // already cached
420            }
421        }
422
423        // Evict until we have room
424        while inner.slots.len() >= inner.max_slots
425            || (inner.current_bytes + data_len > inner.max_bytes && !inner.slots.is_empty())
426        {
427            // Find LRU slot
428            let lru_idx = inner
429                .slots
430                .iter()
431                .enumerate()
432                .min_by_key(|(_, s)| s.last_access)
433                .map(|(i, _)| i)
434                .unwrap();
435            let removed = inner.slots.swap_remove(lru_idx);
436            inner.current_bytes -= removed.data.len();
437        }
438
439        inner.current_bytes += data_len;
440        inner.slots.push(CachedChunk {
441            coord,
442            data,
443            last_access: tick,
444        });
445    }
446
447    /// Clear the entire cache (index + decompressed data).
448    pub fn clear(&self) {
449        let mut inner = self.inner.lock().unwrap();
450        inner.index = None;
451        inner.slots.clear();
452        inner.current_bytes = 0;
453        inner.tick = 0;
454        inner.last_coord = None;
455        inner.stats = AccessStats::default();
456    }
457
458    /// Hint that the given chunk coordinates will be accessed soon.
459    ///
460    /// Pre-populates the chunk index for these coordinates so that
461    /// subsequent lookups are O(1). This does NOT pre-decompress the
462    /// chunks — it only ensures the index entries exist.
463    pub fn prefetch_hint(&self, next_coords: &[ChunkCoord]) {
464        let inner = self.inner.lock().unwrap();
465        if inner.index.is_none() {
466            return;
467        }
468        drop(inner);
469        // For each predicted coordinate, verify it exists in the index.
470        // The index is already populated, so this is a no-op for known chunks.
471        // The purpose is to signal intent — callers can pre-decompress if needed.
472        // We touch the stats to record that prefetch hints were issued.
473        let mut inner = self.inner.lock().unwrap();
474        for coord in next_coords {
475            let exists = inner.index.as_ref()
476                .map(|idx| idx.contains_key(coord))
477                .unwrap_or(false);
478            if exists {
479                inner.stats.sequential_count += 1;
480            }
481        }
482    }
483
484    /// Return the current access pattern statistics.
485    pub fn access_stats(&self) -> AccessStats {
486        self.inner.lock().unwrap().stats.clone()
487    }
488
489    /// Update the sweep direction label in the access stats.
490    pub fn set_sweep_direction(&self, direction: &'static str) {
491        self.inner.lock().unwrap().stats.sweep_direction = Some(direction);
492    }
493
494    /// Number of decompressed chunks currently cached.
495    pub fn cached_chunk_count(&self) -> usize {
496        self.inner.lock().unwrap().slots.len()
497    }
498
499    /// Total bytes of decompressed data currently cached.
500    pub fn cached_bytes(&self) -> usize {
501        self.inner.lock().unwrap().current_bytes
502    }
503}
504
505impl Default for ChunkCache {
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511// ---------------------------------------------------------------------------
512// Tests
513// ---------------------------------------------------------------------------
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    fn make_chunk(offsets: Vec<u64>, address: u64, size: u32) -> ChunkInfo {
520        ChunkInfo {
521            chunk_size: size,
522            filter_mask: 0,
523            offsets,
524            address,
525        }
526    }
527
528    #[test]
529    fn index_populate_and_lookup() {
530        let cache = ChunkCache::new();
531        let chunks = vec![
532            make_chunk(vec![0, 0, 0], 0x1000, 80),
533            make_chunk(vec![10, 0, 0], 0x2000, 80),
534        ];
535        cache.populate_index(&chunks, 2); // rank=2, truncate to [0,0] and [10,0]
536        assert!(cache.has_index());
537
538        let c0 = cache.lookup_index(&[0, 0]).unwrap();
539        assert_eq!(c0.address, 0x1000);
540
541        let c1 = cache.lookup_index(&[10, 0]).unwrap();
542        assert_eq!(c1.address, 0x2000);
543
544        assert!(cache.lookup_index(&[5, 0]).is_none());
545    }
546
547    #[test]
548    fn decompressed_cache_hit() {
549        let cache = ChunkCache::new();
550        cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4]);
551        let got = cache.get_decompressed(&[0, 0]).unwrap();
552        assert_eq!(got, vec![1, 2, 3, 4]);
553    }
554
555    #[test]
556    fn lru_eviction_by_slots() {
557        let cache = ChunkCache::with_capacity(1024 * 1024, 2); // max 2 slots
558
559        cache.put_decompressed(vec![0], vec![1; 10]);
560        cache.put_decompressed(vec![1], vec![2; 10]);
561        assert_eq!(cache.cached_chunk_count(), 2);
562
563        // Access slot 0 to make it more recent
564        cache.get_decompressed(&[0]);
565
566        // Insert slot 2 — should evict slot 1 (LRU)
567        cache.put_decompressed(vec![2], vec![3; 10]);
568        assert_eq!(cache.cached_chunk_count(), 2);
569
570        assert!(cache.get_decompressed(&[0]).is_some());
571        assert!(cache.get_decompressed(&[1]).is_none()); // evicted
572        assert!(cache.get_decompressed(&[2]).is_some());
573    }
574
575    #[test]
576    fn lru_eviction_by_bytes() {
577        let cache = ChunkCache::with_capacity(50, 100); // 50 bytes max
578
579        cache.put_decompressed(vec![0], vec![0; 20]);
580        cache.put_decompressed(vec![1], vec![0; 20]);
581        assert_eq!(cache.cached_bytes(), 40);
582
583        // This needs 20 bytes but only 10 free — evict LRU
584        cache.put_decompressed(vec![2], vec![0; 20]);
585        assert!(cache.cached_bytes() <= 50);
586        assert!(cache.get_decompressed(&[0]).is_none()); // evicted (LRU)
587    }
588
589    #[test]
590    fn oversized_chunk_not_cached() {
591        let cache = ChunkCache::with_capacity(10, 16);
592        cache.put_decompressed(vec![0], vec![0; 100]); // too big
593        assert_eq!(cache.cached_chunk_count(), 0);
594    }
595
596    #[test]
597    fn clear_resets_everything() {
598        let cache = ChunkCache::new();
599        let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
600        cache.populate_index(&chunks, 1);
601        cache.put_decompressed(vec![0], vec![1, 2, 3]);
602
603        cache.clear();
604        assert!(!cache.has_index());
605        assert_eq!(cache.cached_chunk_count(), 0);
606        assert_eq!(cache.cached_bytes(), 0);
607    }
608
609    #[test]
610    fn duplicate_insert_is_noop() {
611        let cache = ChunkCache::new();
612        cache.put_decompressed(vec![0], vec![1, 2, 3]);
613        cache.put_decompressed(vec![0], vec![1, 2, 3]); // duplicate
614        assert_eq!(cache.cached_chunk_count(), 1);
615        assert_eq!(cache.cached_bytes(), 3);
616    }
617
618    // --- CacheAlignedBuffer tests ---
619
620    #[test]
621    fn aligned_buffer_basic() {
622        let buf = CacheAlignedBuffer::zeroed(256);
623        assert_eq!(buf.len(), 256);
624        assert!(buf.is_aligned());
625        assert_eq!(&buf[..4], &[0, 0, 0, 0]);
626    }
627
628    #[test]
629    fn aligned_buffer_from_slice() {
630        let data = vec![1u8, 2, 3, 4, 5];
631        let buf = CacheAlignedBuffer::from_slice(&data);
632        assert_eq!(buf.len(), 5);
633        assert!(buf.is_aligned());
634        assert_eq!(buf.to_vec(), data);
635    }
636
637    #[test]
638    fn aligned_buffer_from_vec() {
639        let data = vec![42u8; 1024];
640        let buf = CacheAlignedBuffer::from_vec(data.clone());
641        assert!(buf.is_aligned());
642        assert_eq!(buf.to_vec(), data);
643    }
644
645    #[test]
646    fn aligned_buffer_empty() {
647        let buf = CacheAlignedBuffer::zeroed(0);
648        assert!(buf.is_empty());
649        assert!(buf.is_aligned());
650        assert_eq!(buf.to_vec(), Vec::<u8>::new());
651    }
652
653    #[test]
654    fn aligned_buffer_clone_is_aligned() {
655        let buf = CacheAlignedBuffer::from_slice(&[1, 2, 3, 4]);
656        let cloned = buf.clone();
657        assert!(cloned.is_aligned());
658        assert_eq!(buf.to_vec(), cloned.to_vec());
659    }
660
661    #[test]
662    fn aligned_buffer_deref_works() {
663        let buf = CacheAlignedBuffer::from_slice(&[10, 20, 30]);
664        assert_eq!(buf[0], 10);
665        assert_eq!(buf[1], 20);
666        assert_eq!(buf[2], 30);
667    }
668
669    #[test]
670    fn aligned_buffer_various_sizes() {
671        // Test alignment for various sizes including non-power-of-two
672        for size in [1, 7, 63, 64, 65, 127, 128, 129, 255, 256, 1000, 4096] {
673            let buf = CacheAlignedBuffer::zeroed(size);
674            assert!(buf.is_aligned(), "not aligned for size {size}");
675            assert_eq!(buf.len(), size);
676        }
677    }
678
679    #[test]
680    fn cached_data_is_aligned() {
681        let cache = ChunkCache::new();
682        cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4, 5, 6, 7, 8]);
683        let aligned = cache.get_decompressed_aligned(&[0, 0]).unwrap();
684        assert!(aligned.is_aligned());
685        assert_eq!(aligned.to_vec(), vec![1, 2, 3, 4, 5, 6, 7, 8]);
686    }
687
688    #[test]
689    fn align_to_cache_line_values() {
690        assert_eq!(align_to_cache_line(0), 0);
691        assert_eq!(align_to_cache_line(1), CACHE_LINE_SIZE);
692        assert_eq!(align_to_cache_line(CACHE_LINE_SIZE), CACHE_LINE_SIZE);
693        assert_eq!(align_to_cache_line(CACHE_LINE_SIZE + 1), CACHE_LINE_SIZE * 2);
694    }
695}