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                for entry in fs_err::read_dir(cpu_path.join("cache"))? {
185                    let entry = entry?;
186                    let path = entry.path();
187                    if !path
188                        .file_name()
189                        .unwrap()
190                        .to_str()
191                        .is_some_and(|s| s.starts_with("index"))
192                    {
193                        continue;
194                    }
195
196                    let associativity = fs_err::read_to_string(path.join("ways_of_associativity"))?
197                        .trim_end()
198                        .parse()
199                        .unwrap();
200
201                    let mut cpus = Vec::new();
202                    for range in fs_err::read_to_string(path.join("shared_cpu_list"))?
203                        .trim_end()
204                        .split(',')
205                    {
206                        if let Some((start, end)) = range.split_once('-') {
207                            cpus.extend(
208                                start.parse::<u32>().unwrap()..=end.parse::<u32>().unwrap(),
209                            );
210                        } else {
211                            cpus.push(range.parse().unwrap());
212                        }
213                    }
214
215                    let line_size_result = fs_err::read_to_string(path.join("coherency_line_size"));
216                    let line_size = match line_size_result {
217                        Ok(s) => s.trim_end().parse::<u32>().unwrap(),
218                        Err(e) => match e.kind() {
219                            std::io::ErrorKind::NotFound => 64,
220                            _ => return std::io::Result::Err(e),
221                        },
222                    };
223                    caches.push(Cache {
224                        cpus,
225                        level: fs_err::read_to_string(path.join("level"))?
226                            .trim_end()
227                            .parse()
228                            .unwrap(),
229                        cache_type: match fs_err::read_to_string(path.join("type"))?.trim_end() {
230                            "Data" => super::CacheType::Data,
231                            "Instruction" => super::CacheType::Instruction,
232                            "Unified" => super::CacheType::Unified,
233                            _ => continue,
234                        },
235                        size: fs_err::read_to_string(path.join("size"))?
236                            .strip_suffix("K\n")
237                            .unwrap()
238                            .parse::<u32>()
239                            .unwrap()
240                            * 1024,
241                        associativity: if associativity == 0 {
242                            None
243                        } else {
244                            Some(associativity)
245                        },
246                        line_size,
247                    });
248                }
249            }
250            Ok(caches)
251        }
252    }
253}
254
255#[cfg(target_os = "macos")]
256mod macos {
257    use super::Cache;
258    use super::CacheTopology;
259
260    impl CacheTopology {
261        pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
262            // TODO
263            Ok(Vec::new())
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    #[test]
271    fn test_host_cache_topology() {
272        let topology = super::CacheTopology::from_host().unwrap();
273        assert!(!topology.caches.is_empty());
274        println!("{topology:?}");
275    }
276}