Skip to main content

commonware_utils/
prefetch.rs

1//! Cache-line prefetch hints and a lookahead cursor for software-pipelined scans.
2//!
3//! A scan whose per-item work waits on a cache miss can issue that miss for an item some distance
4//! ahead of consuming it, so several misses are in flight at once. [Lookahead] hands the scan each
5//! item that distance ahead of consuming it, and [prefetch] and [prefetch_lines] issue the hints.
6//!
7//! # Examples
8//!
9//! ```
10//! use commonware_utils::prefetch::Lookahead;
11//!
12//! let items = vec![1u64, 2, 3, 4, 5];
13//! let mut hinted = Vec::new();
14//! let mut ahead = Lookahead::new(2);
15//! let mut scan = items.into_iter();
16//! loop {
17//!     // Each item comes up once, two items before the scan consumes it.
18//!     for item in ahead.advance(scan.as_slice()) {
19//!         hinted.push(*item);
20//!     }
21//!     let Some(item) = scan.next() else { break };
22//!     assert!(hinted.contains(&item));
23//! }
24//! assert_eq!(hinted, [1, 2, 3, 4, 5]);
25//! ```
26
27/// Bytes per cache line, the granularity of [prefetch_lines].
28const CACHE_LINE: usize = 64;
29
30/// Whether [prefetch] issues a hint on this target. Hint sites return early when it does not, so
31/// the overflow and bounds checks on the addresses they compute are compiled out with the hint.
32pub const ENABLED: bool = cfg!(all(
33    any(target_arch = "x86_64", target_arch = "aarch64"),
34    not(miri)
35));
36
37/// Hint the CPU to start loading the cache line holding `ptr`, without waiting for it. A prefetch
38/// never faults and has no architectural effect, so `ptr` may be dangling or out of bounds. A no-op
39/// on targets other than x86_64 and aarch64, and under Miri.
40// Not const because `_mm_prefetch` is const-callable only on nightly toolchains.
41#[allow(clippy::missing_const_for_fn)]
42#[inline(always)]
43pub fn prefetch<T>(ptr: *const T) {
44    cfg_if::cfg_if! {
45        if #[cfg(all(target_arch = "x86_64", not(miri)))] {
46            // SAFETY: `prefetcht0` is a hint that never faults and reads nothing architecturally,
47            // so any address is sound.
48            unsafe {
49                core::arch::x86_64::_mm_prefetch::<{ core::arch::x86_64::_MM_HINT_T0 }>(ptr.cast());
50            }
51        } else if #[cfg(all(target_arch = "aarch64", not(miri)))] {
52            // SAFETY: `prfm` is a hint that never faults and reads nothing architecturally, so any
53            // address is sound.
54            unsafe {
55                core::arch::asm!(
56                    "prfm pldl1keep, [{ptr}]",
57                    ptr = in(reg) ptr,
58                    options(nostack, readonly, preserves_flags),
59                );
60            }
61        } else {
62            let _ = ptr;
63        }
64    }
65}
66
67/// Hint every cache line that the `len` values of `T` at `ptr` intersect, at most `max_lines` of
68/// them from the first (see [prefetch]).
69#[inline]
70pub fn prefetch_lines<T>(ptr: *const T, len: usize, max_lines: usize) {
71    if !ENABLED {
72        return;
73    }
74    for line in lines(ptr.cast(), len.saturating_mul(size_of::<T>()), max_lines) {
75        prefetch(line);
76    }
77}
78
79/// The start of each cache line that the `len` bytes at `ptr` intersect, at most `max_lines` of
80/// them from the first. A span that starts partway into a line can reach one line further than
81/// its length alone would.
82#[inline]
83fn lines(ptr: *const u8, len: usize, max_lines: usize) -> impl Iterator<Item = *const u8> {
84    let misalign = ptr.addr() % CACHE_LINE;
85    let count = if len == 0 {
86        0
87    } else {
88        misalign.saturating_add(len).div_ceil(CACHE_LINE)
89    };
90    let first = ptr.wrapping_sub(misalign);
91    (0..count.min(max_lines)).map(move |line| first.wrapping_add(line * CACHE_LINE))
92}
93
94/// A cursor that runs a fixed distance ahead of a scan, handing out each item that many items
95/// before the scan consumes it, so the caller can prefetch what the item's work reads.
96///
97/// Call [Self::advance] with the scan's unconsumed tail before consuming its next item. It
98/// returns every item exactly once, in scan order, and the first call returns the scan's first
99/// `distance + 1` items so the pipeline starts full.
100///
101/// Over a scan of eight items at distance two, the call before each item returns:
102///
103/// ```text
104/// before item 0   remaining [0 1 2 3 4 5 6 7]   returns [0 1 2]   (fills the window)
105/// before item 1   remaining   [1 2 3 4 5 6 7]   returns [3]
106/// before item 2   remaining     [2 3 4 5 6 7]   returns [4]
107/// ...
108/// before item 5   remaining           [5 6 7]   returns [7]
109/// before item 6   remaining             [6 7]   returns []        (the tail is spent)
110/// ```
111#[derive(Clone, Debug)]
112pub struct Lookahead {
113    /// Items handed out ahead of the one the scan consumes next.
114    distance: usize,
115    /// Items at the end of the scan that no call has returned: all of them before the first
116    /// call, whatever the scan's length.
117    unreturned: usize,
118}
119
120impl Lookahead {
121    /// A cursor over one scan that runs `distance` items ahead.
122    pub const fn new(distance: usize) -> Self {
123        Self {
124            distance,
125            unreturned: usize::MAX,
126        }
127    }
128
129    /// The items within `distance` of the scan's next item that no earlier call returned.
130    /// `remaining` holds the scan's unconsumed tail, in order.
131    ///
132    /// # Panics
133    ///
134    /// Panics if `remaining` starts more than `distance + 1` items before the first item no
135    /// earlier call returned.
136    pub fn advance<'a, T>(&mut self, remaining: &'a [T]) -> &'a [T] {
137        let reach = remaining.len().min(self.distance.saturating_add(1));
138        let returned = remaining.len().saturating_sub(self.unreturned);
139        self.unreturned = remaining.len() - reach;
140        &remaining[returned..reach]
141    }
142}
143
144#[cfg(test)]
145#[commonware_macros::test_group("miri")]
146mod tests {
147    use super::*;
148
149    /// The lines [lines] yields for a span `start` bytes past a line boundary, as offsets from that
150    /// boundary.
151    fn line_offsets(start: usize, len: usize, max_lines: usize) -> Vec<usize> {
152        let base = core::ptr::without_provenance::<u8>(0x10000);
153        lines(base.wrapping_add(start), len, max_lines)
154            .map(|line| line.addr() - base.addr())
155            .collect()
156    }
157
158    #[test]
159    fn test_lines_cover_every_intersected_line() {
160        // Aligned spans take exactly the lines their length needs.
161        assert_eq!(line_offsets(0, 64, 8), [0]);
162        assert_eq!(line_offsets(0, 65, 8), [0, 64]);
163
164        // Unaligned spans also take the line their tail reaches into.
165        assert_eq!(line_offsets(16, 64, 8), [0, 64]);
166        assert_eq!(line_offsets(16, 128, 8), [0, 64, 128]);
167        assert_eq!(line_offsets(63, 2, 8), [0, 64]);
168
169        // An empty span takes no line, and the cap bounds a long one.
170        assert!(line_offsets(16, 0, 8).is_empty());
171        assert_eq!(
172            line_offsets(16, 1024, 8),
173            [0, 64, 128, 192, 256, 320, 384, 448]
174        );
175
176        // The longest span the signature admits counts its lines without overflow.
177        assert_eq!(line_offsets(1, usize::MAX, 2), [0, 64]);
178        assert!(line_offsets(1, usize::MAX, 0).is_empty());
179    }
180
181    #[test]
182    fn test_lookahead_returns_each_item_once_distance_ahead() {
183        // Empty and one-item scans, and distances of zero, within the scan, past its end, and the
184        // largest.
185        for (total, distance) in [(0, 2), (1, 2), (5, 0), (5, 2), (5, 8), (5, usize::MAX)] {
186            // Advance the cursor before consuming each item, collecting what it hands out.
187            let items: Vec<usize> = (0..total).collect();
188            let mut ahead = Lookahead::new(distance);
189            let mut scan = items.into_iter();
190            let mut hinted = Vec::new();
191            let mut consumed = 0;
192            loop {
193                // Everything up to `distance` past the next item has come up, and nothing beyond.
194                hinted.extend_from_slice(ahead.advance(scan.as_slice()));
195                assert_eq!(
196                    hinted.len(),
197                    total.min(distance.saturating_add(consumed + 1)),
198                    "{total} {distance}"
199                );
200                let Some(item) = scan.next() else { break };
201                assert_eq!(item, consumed);
202                consumed += 1;
203            }
204
205            // Every item came up exactly once, in scan order.
206            assert_eq!(hinted, (0..total).collect::<Vec<_>>(), "{total} {distance}");
207        }
208    }
209
210    #[test]
211    fn test_lookahead_never_repeats_an_item() {
212        // The first call fills the pipeline, and a call with the same tail hands out nothing more.
213        let items = [0, 1, 2, 3];
214        let mut ahead = Lookahead::new(1);
215        assert_eq!(ahead.advance(&items), [0, 1]);
216        assert!(ahead.advance(&items).is_empty());
217
218        // An item the scan consumed before any call reached it is never returned.
219        assert_eq!(ahead.advance(&items[3..]), [3]);
220        assert!(ahead.advance(&items[4..]).is_empty());
221    }
222
223    #[test]
224    fn test_prefetch_accepts_any_address() {
225        // A null address, and a span that runs past the end of the address space.
226        prefetch(core::ptr::null::<u8>());
227        prefetch_lines(core::ptr::without_provenance::<u64>(usize::MAX - 8), 64, 8);
228    }
229}