Skip to main content

cache_topology/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Provides ways to describe a machine's cache topology and to query it from
5//! the current running machine.
6
7use thiserror::Error;
8
9/// A machine's cache topology.
10#[derive(Debug)]
11pub struct CacheTopology {
12    /// A list of caches.
13    pub caches: Vec<Cache>,
14}
15
16/// A memory cache.
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct Cache {
19    /// The cache level, 1 being closest to the CPU.
20    pub level: u8,
21    /// The cache type.
22    pub cache_type: CacheType,
23    /// The CPUs that share this cache.
24    pub cpus: Vec<u32>,
25    /// The cache size in bytes.
26    pub size: u32,
27    /// The cache associativity. /// If `None`, this cache is fully associative.
28    pub associativity: Option<u32>,
29    /// The cache line size in bytes.
30    pub line_size: u32,
31}
32
33/// A cache type.
34#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
35pub enum CacheType {
36    /// A data cache.
37    Data,
38    /// An instruction cache.
39    Instruction,
40    /// A unified cache.
41    Unified,
42}
43
44/// An error returned by [`CacheTopology::from_host`].
45#[derive(Debug, Error)]
46pub enum HostTopologyError {
47    /// An error occurred while retrieving the cache topology.
48    #[error("os error retrieving cache topology")]
49    Os(#[source] std::io::Error),
50}
51
52impl CacheTopology {
53    /// Returns the cache topology of the current machine.
54    pub fn from_host() -> Result<Self, HostTopologyError> {
55        let mut caches = Self::host_caches().map_err(HostTopologyError::Os)?;
56        caches.sort();
57        caches.dedup();
58        Ok(Self { caches })
59    }
60}
61
62#[cfg(windows)]
63// UNSAFETY: needed to call Win32 functions to query cache topology
64#[expect(unsafe_code)]
65mod windows {
66    use super::CacheTopology;
67    use crate::Cache;
68    use crate::CacheType;
69    use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
70    use windows_sys::Win32::System::SystemInformation;
71
72    impl CacheTopology {
73        pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
74            let mut len = 0;
75            // SAFETY: passing a zero-length buffer as allowed by this routine.
76            let r = unsafe {
77                SystemInformation::GetLogicalProcessorInformationEx(
78                    SystemInformation::RelationCache,
79                    std::ptr::null_mut(),
80                    &mut len,
81                )
82            };
83            assert_eq!(r, 0);
84            let err = std::io::Error::last_os_error();
85            if err.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) {
86                return Err(err);
87            }
88            let mut buf = vec![0u8; len as usize];
89            // SAFETY: passing a buffer of the correct size as returned by the
90            // previous call.
91            let r = unsafe {
92                SystemInformation::GetLogicalProcessorInformationEx(
93                    SystemInformation::RelationCache,
94                    buf.as_mut_ptr().cast(),
95                    &mut len,
96                )
97            };
98            if r == 0 {
99                return Err(std::io::Error::last_os_error());
100            }
101
102            let mut caches = Vec::new();
103
104            let mut buf = buf.as_slice();
105            while !buf.is_empty() {
106                // SAFETY: the remaining buffer is guaranteed to be large enough to hold
107                // the structure.
108                let info = unsafe {
109                    &*buf
110                        .as_ptr()
111                        .cast::<SystemInformation::SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>()
112                };
113
114                assert_eq!(info.Relationship, SystemInformation::RelationCache);
115                buf = &buf[info.Size as usize..];
116
117                // SAFETY: this is a cache entry, as guaranteed by the previous
118                // assertion.
119                let cache = unsafe { &info.Anonymous.Cache };
120
121                // SAFETY: the buffer is guaranteed by Win32 to be large enough
122                // to hold the group masks.
123                let groups = unsafe {
124                    std::slice::from_raw_parts(
125                        cache.Anonymous.GroupMasks.as_ptr(),
126                        cache.GroupCount as usize,
127                    )
128                };
129
130                let mut cpus = Vec::new();
131                for group in groups {
132                    for i in 0..usize::BITS {
133                        if group.Mask & (1 << i) != 0 {
134                            cpus.push(group.Group as u32 * usize::BITS + i);
135                        }
136                    }
137                }
138
139                caches.push(Cache {
140                    cpus,
141                    level: cache.Level,
142                    cache_type: match cache.Type {
143                        SystemInformation::CacheUnified => CacheType::Unified,
144                        SystemInformation::CacheInstruction => CacheType::Instruction,
145                        SystemInformation::CacheData => CacheType::Data,
146                        _ => continue,
147                    },
148                    size: cache.CacheSize,
149                    associativity: if cache.Associativity == !0 {
150                        None
151                    } else {
152                        Some(cache.Associativity.into())
153                    },
154                    line_size: cache.LineSize.into(),
155                });
156            }
157
158            Ok(caches)
159        }
160    }
161}
162
163#[cfg(target_os = "linux")]
164mod linux {
165    use super::Cache;
166    use super::CacheTopology;
167
168    impl CacheTopology {
169        pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
170            let mut caches = Vec::new();
171            for cpu_entry in fs_err::read_dir("/sys/devices/system/cpu")? {
172                let cpu_path = cpu_entry?.path();
173                if cpu_path
174                    .file_name()
175                    .unwrap()
176                    .to_str()
177                    .unwrap()
178                    .strip_prefix("cpu")
179                    .and_then(|s| s.parse::<u32>().ok())
180                    .is_none()
181                {
182                    continue;
183                }
184                let cache_dir = cpu_path.join("cache");
185                let cache_entries = match fs_err::read_dir(&cache_dir) {
186                    Ok(entries) => entries,
187                    Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
188                    Err(e) => return Err(e),
189                };
190                for entry in cache_entries {
191                    let entry = entry?;
192                    let path = entry.path();
193                    if !path
194                        .file_name()
195                        .unwrap()
196                        .to_str()
197                        .is_some_and(|s| s.starts_with("index"))
198                    {
199                        continue;
200                    }
201
202                    // Some environments (e.g. QEMU) don't expose all cache
203                    // sysfs files. Skip entries that are missing required files.
204                    let read_optional_file = |name: &str| -> std::io::Result<Option<String>> {
205                        match fs_err::read_to_string(path.join(name)) {
206                            Ok(s) => Ok(Some(s)),
207                            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
208                            Err(e) => Err(e),
209                        }
210                    };
211
212                    let Some(assoc_str) = read_optional_file("ways_of_associativity")? else {
213                        continue;
214                    };
215                    let associativity: u32 = assoc_str.trim_end().parse().unwrap();
216
217                    let Some(cpu_list_str) = read_optional_file("shared_cpu_list")? else {
218                        continue;
219                    };
220                    let mut cpus = Vec::new();
221                    for range in cpu_list_str.trim_end().split(',') {
222                        if let Some((start, end)) = range.split_once('-') {
223                            cpus.extend(
224                                start.parse::<u32>().unwrap()..=end.parse::<u32>().unwrap(),
225                            );
226                        } else {
227                            cpus.push(range.parse().unwrap());
228                        }
229                    }
230
231                    let line_size = match read_optional_file("coherency_line_size")? {
232                        Some(s) => s.trim_end().parse::<u32>().unwrap(),
233                        None => 64,
234                    };
235
236                    let Some(level_str) = read_optional_file("level")? else {
237                        continue;
238                    };
239                    let Some(type_str) = read_optional_file("type")? else {
240                        continue;
241                    };
242                    let Some(size_str) = read_optional_file("size")? else {
243                        continue;
244                    };
245
246                    caches.push(Cache {
247                        cpus,
248                        level: level_str.trim_end().parse().unwrap(),
249                        cache_type: match type_str.trim_end() {
250                            "Data" => super::CacheType::Data,
251                            "Instruction" => super::CacheType::Instruction,
252                            "Unified" => super::CacheType::Unified,
253                            _ => continue,
254                        },
255                        size: size_str
256                            .strip_suffix("K\n")
257                            .unwrap()
258                            .parse::<u32>()
259                            .unwrap()
260                            * 1024,
261                        associativity: if associativity == 0 {
262                            None
263                        } else {
264                            Some(associativity)
265                        },
266                        line_size,
267                    });
268                }
269            }
270            Ok(caches)
271        }
272    }
273}
274
275#[cfg(target_os = "macos")]
276mod macos {
277    use super::Cache;
278    use super::CacheTopology;
279
280    impl CacheTopology {
281        pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
282            // TODO
283            Ok(Vec::new())
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    #[test]
291    fn test_host_cache_topology() {
292        let topology = super::CacheTopology::from_host().unwrap();
293        assert!(!topology.caches.is_empty());
294        println!("{topology:?}");
295    }
296}