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}