Skip to main content

prefetch_index/
lib.rs

1//! A small crate to prefetch an element of an array.
2//!
3//! Provides [`prefetch_index`] and [`prefetch_index_nta`] to prefetch the cache
4//! line containing `slice[index]`.
5
6#![cfg_attr(
7    all(feature = "aarch64", target_arch = "aarch64"),
8    feature(stdarch_aarch64_prefetch)
9)]
10
11/// Prefetches the cache line containing (the first byte of) `data[index]` into
12/// _all_ levels of the cache.
13///
14/// On x86/x86_64, this uses _mm_prefetch which only requires the (commonly available) SSE feature.
15///
16/// On aarch64, this uses assembly intrinsics, or the nightly `aarch64::_prefetch` instruction when the `aarch64` feature is used.
17///
18/// On other architectures, this is a no-op.
19///
20/// ```
21/// use prefetch_index::prefetch_index;
22/// let data = vec![0u8; 1024];
23/// prefetch_index(&data, 512);
24/// ```
25#[inline(always)]
26pub fn prefetch_index<T>(data: impl AsRef<[T]>, index: usize) {
27    let ptr = data.as_ref().as_ptr().wrapping_add(index) as *const i8;
28    #[cfg(all(target_arch = "x86_64", target_feature = "sse"))]
29    unsafe {
30        std::arch::x86_64::_mm_prefetch(ptr, std::arch::x86_64::_MM_HINT_T0);
31    }
32    #[cfg(all(target_arch = "x86", target_feature = "sse"))]
33    unsafe {
34        std::arch::x86::_mm_prefetch(ptr, std::arch::x86::_MM_HINT_T0);
35    }
36    #[cfg(all(target_arch = "aarch64", feature = "aarch64"))]
37    unsafe {
38        std::arch::aarch64::_prefetch::<
39            { std::arch::aarch64::_PREFETCH_READ },
40            { std::arch::aarch64::_PREFETCH_LOCALITY3 },
41        >(ptr);
42    }
43    #[cfg(all(target_arch = "aarch64", not(feature = "aarch64")))]
44    unsafe {
45        core::arch::asm!(
46            "prfm pldl1keep, [{p}]",
47            p = in(reg) ptr,
48            options(nostack, preserves_flags, readonly),
49        );
50    }
51    #[cfg(not(any(
52        all(target_arch = "x86_64", target_feature = "sse"),
53        all(target_arch = "x86", target_feature = "sse"),
54        all(target_arch = "aarch64")
55    )))]
56    {
57        let _ = ptr; // Silence unused variable warning.
58    }
59}
60
61/// Prefetches the cache line containing (the first byte of) `data[index]` for non-temporal access.
62///
63/// On x86/x86_64, this uses _mm_prefetch which only requires the (commonly available) SSE feature.
64///
65/// On aarch64, this is gated behind the `aarch64` feature flag as `aarch64::_prefetch` is nightly.
66///
67/// On other architectures, this is a no-op.
68#[inline(always)]
69pub fn prefetch_index_nta<T>(data: impl AsRef<[T]>, index: usize) {
70    let ptr = data.as_ref().as_ptr().wrapping_add(index) as *const i8;
71    #[cfg(all(target_arch = "x86_64", target_feature = "sse"))]
72    unsafe {
73        std::arch::x86_64::_mm_prefetch(ptr, std::arch::x86_64::_MM_HINT_NTA);
74    }
75    #[cfg(all(target_arch = "x86", target_feature = "sse"))]
76    unsafe {
77        std::arch::x86::_mm_prefetch(ptr, std::arch::x86::_MM_HINT_NTA);
78    }
79    #[cfg(all(target_arch = "aarch64", feature = "aarch64"))]
80    unsafe {
81        std::arch::aarch64::_prefetch::<
82            { std::arch::aarch64::_PREFETCH_READ },
83            { std::arch::aarch64::_PREFETCH_LOCALITY0 },
84        >(ptr);
85    }
86    #[cfg(all(target_arch = "aarch64", not(feature = "aarch64")))]
87    unsafe {
88        core::arch::asm!(
89            "prfm pldl1strm, [{p}]",
90            p = in(reg) ptr,
91            options(nostack, preserves_flags, readonly),
92        );
93    }
94    #[cfg(not(any(
95        all(target_arch = "x86_64", target_feature = "sse"),
96        all(target_arch = "x86", target_feature = "sse"),
97        all(target_arch = "aarch64")
98    )))]
99    {
100        let _ = ptr; // Silence unused variable warning.
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::{array::from_fn, hint::black_box};
107
108    use super::*;
109
110    #[test]
111    fn basic() {
112        let data = vec![0u8; 1024];
113        prefetch_index(&data, 512);
114        prefetch_index_nta(&data, 512);
115    }
116
117    /// Returns a large vector with each index pointing to the next cache line.
118    fn setup() -> Vec<u64> {
119        let len = 1024 * 1024 * 1024 / 8;
120        let mut data = vec![0u64; len];
121        // triangular stride hits all position:
122        // 0->1->3->6->...
123        let mut idx = 0;
124        for i in 0..len {
125            let next = (idx + (i + 1)) % len;
126            assert_eq!(data[idx], 0);
127            data[idx] = next as u64;
128            idx = next;
129        }
130        data
131    }
132
133    #[test]
134    fn out_of_bounds_is_ok() {
135        for j in 0..20 {
136            let len = 1 << j;
137            let data = vec![0u64; len];
138            for i in 0..100usize {
139                prefetch_index(&data, i.wrapping_neg());
140                prefetch_index(&data, len + i);
141            }
142            prefetch_index(&data, usize::MAX);
143            prefetch_index(&data, len + usize::MAX / 2);
144        }
145    }
146
147    #[test]
148    fn batched_pointer_chasing_prefetch() {
149        // batch size
150        const B: usize = 32;
151
152        let data = setup();
153
154        let time = |prefetch: bool| {
155            let mut indices: [usize; B] = from_fn(|i| i * data.len() / B + i);
156            let start = std::time::Instant::now();
157            let mut sum = 0;
158            for _ in 0..data.len() / B {
159                for idx in &mut indices {
160                    *idx = data[*idx] as usize;
161                    if prefetch {
162                        prefetch_index(&data, *idx);
163                    }
164                    // Simulate some work to fill the CPU reorder buffer.
165                    let mut x: usize = 1;
166                    for _ in 0..16 {
167                        x = x.wrapping_mul(*idx);
168                        sum += x;
169                    }
170                }
171            }
172            black_box(sum);
173            let duration = start.elapsed();
174            let ns_per_it = duration.as_nanos() as f64 / (data.len() as f64);
175            eprintln!(
176                "Prefetch={prefetch:<5}:    {:?}  {:5.2} ns/it",
177                duration, ns_per_it
178            );
179            ns_per_it
180        };
181        let no_prefetch = time(false);
182        let with_prefetch = time(true);
183        assert!(with_prefetch < 0.8 * no_prefetch, "Time with prefetching {with_prefetch} is not sufficiently smaller than time without prefetching {no_prefetch}");
184    }
185}