use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CacheInfo {
pub l1_data: usize,
pub l2: usize,
pub l3: usize,
}
impl CacheInfo {
pub fn l1_data_or_default(&self) -> usize {
if self.l1_data > 0 {
self.l1_data
} else if cfg!(target_arch = "aarch64") {
64 * 1024
} else {
32 * 1024
}
}
pub fn l2_or_default(&self) -> usize {
if self.l2 > 0 { self.l2 } else { 256 * 1024 }
}
}
pub fn cache_info() -> CacheInfo {
static CACHE: OnceLock<CacheInfo> = OnceLock::new();
*CACHE.get_or_init(detect)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LlcKind {
Dedicated,
SystemLevel,
}
pub fn last_level_cache() -> Option<(usize, LlcKind)> {
static LLC: OnceLock<Option<(usize, LlcKind)>> = OnceLock::new();
*LLC.get_or_init(|| {
let ci = cache_info();
let override_bytes = env_llc_override();
let slc =
if override_bytes.is_some() || ci.l3 > ci.l2 { 0 } else { system_level_cache_bytes() };
resolve_llc(
override_bytes,
std::env::var_os("TRACT_LLC_CONTENDED").is_some(),
ci.l2,
ci.l3,
slc,
)
})
}
fn resolve_llc(
override_bytes: Option<usize>,
override_contended: bool,
l2: usize,
l3: usize,
slc: usize,
) -> Option<(usize, LlcKind)> {
if let Some(b) = override_bytes.filter(|b| *b > 0) {
let kind = if override_contended { LlcKind::SystemLevel } else { LlcKind::Dedicated };
return Some((b, kind));
}
if l3 > l2 {
return Some((l3, LlcKind::Dedicated));
}
if slc > l2 && slc > 0 {
return Some((slc, LlcKind::SystemLevel));
}
None
}
fn env_llc_override() -> Option<usize> {
let b = parse_cache_size(&std::env::var("TRACT_LLC_BYTES").ok()?);
(b > 0).then_some(b)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn system_level_cache_bytes() -> usize {
use std::path::Path;
fn be_u32(p: &Path) -> Option<u32> {
let b = std::fs::read(p).ok()?;
(b.len() >= 4).then(|| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn walk(dir: &Path, depth: usize, best: &mut usize) {
if depth == 0 {
return;
}
if be_u32(&dir.join("cache-level")) == Some(3) {
let sz = be_u32(&dir.join("cache-size")).unwrap_or(0) as usize;
*best = (*best).max(sz);
}
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let p = e.path();
if p.is_dir() && p.file_name().and_then(|n| n.to_str()) != Some("cpus") {
walk(&p, depth - 1, best);
}
}
}
let mut best = 0;
for root in ["/proc/device-tree", "/sys/firmware/devicetree/base"] {
let p = Path::new(root);
if p.exists() {
walk(p, 4, &mut best);
if best > 0 {
break;
}
}
}
best
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn system_level_cache_bytes() -> usize {
0
}
#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
fn parse_cache_size(s: &str) -> usize {
let s = s.trim();
let (num, mult) = if let Some(n) = s.strip_suffix(['K', 'k']) {
(n, 1024)
} else if let Some(n) = s.strip_suffix(['M', 'm']) {
(n, 1024 * 1024)
} else {
(s, 1)
};
num.trim().parse::<usize>().unwrap_or(0) * mult
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
fn detect() -> CacheInfo {
fn sysctl_usize(name: &str) -> Option<usize> {
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};
unsafe extern "C" {
fn sysctlbyname(
name: *const c_char,
oldp: *mut c_void,
oldlenp: *mut usize,
newp: *mut c_void,
newlen: usize,
) -> c_int;
}
let cname = CString::new(name).ok()?;
let mut val: u64 = 0;
let mut len = std::mem::size_of::<u64>();
let rc = unsafe {
sysctlbyname(
cname.as_ptr(),
&mut val as *mut u64 as *mut c_void,
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || val == 0 { None } else { Some(val as usize) }
}
CacheInfo {
l1_data: sysctl_usize("hw.perflevel0.l1dcachesize")
.or_else(|| sysctl_usize("hw.l1dcachesize"))
.unwrap_or(0),
l2: sysctl_usize("hw.perflevel0.l2cachesize")
.or_else(|| sysctl_usize("hw.l2cachesize"))
.unwrap_or(0),
l3: sysctl_usize("hw.perflevel0.l3cachesize")
.or_else(|| sysctl_usize("hw.l3cachesize"))
.unwrap_or(0),
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn detect() -> CacheInfo {
let mut ci = CacheInfo::default();
for idx in 0..16 {
let base = format!("/sys/devices/system/cpu/cpu0/cache/index{idx}/");
let Ok(level) = std::fs::read_to_string(format!("{base}level")) else {
continue;
};
let level: usize = level.trim().parse().unwrap_or(0);
let ctype = std::fs::read_to_string(format!("{base}type"))
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let size = std::fs::read_to_string(format!("{base}size"))
.map(|s| parse_cache_size(&s))
.unwrap_or(0);
if size == 0 {
continue;
}
match level {
1 if ctype == "data" || ctype == "unified" => {
if ci.l1_data == 0 {
ci.l1_data = size;
}
}
2 if ci.l2 == 0 => ci.l2 = size,
3 if ci.l3 == 0 => ci.l3 = size,
_ => {}
}
}
ci
}
#[cfg(target_os = "windows")]
fn detect() -> CacheInfo {
let mut ci = CacheInfo::default();
if let Ok(out) = std::process::Command::new("wmic")
.args(["cpu", "get", "L2CacheSize,L3CacheSize", "/format:value"])
.output()
{
for line in String::from_utf8_lossy(&out.stdout).lines() {
let line = line.trim();
if let Some(v) = line.strip_prefix("L2CacheSize=") {
if let Ok(kb) = v.trim().parse::<usize>() {
ci.l2 = kb * 1024;
}
} else if let Some(v) = line.strip_prefix("L3CacheSize=") {
if let Ok(kb) = v.trim().parse::<usize>() {
ci.l3 = kb * 1024;
}
}
}
}
ci
}
#[cfg(not(any(
target_os = "macos",
target_os = "ios",
target_os = "linux",
target_os = "android",
target_os = "windows"
)))]
fn detect() -> CacheInfo {
CacheInfo::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn llc_resolution_priority() {
assert_eq!(
resolve_llc(Some(8 << 20), false, 1 << 20, 4 << 20, 0),
Some((8 << 20, LlcKind::Dedicated))
);
assert_eq!(
resolve_llc(Some(8 << 20), true, 1 << 20, 0, 0),
Some((8 << 20, LlcKind::SystemLevel))
);
assert_eq!(
resolve_llc(None, false, 1 << 20, 4 << 20, 0),
Some((4 << 20, LlcKind::Dedicated))
);
assert_eq!(
resolve_llc(None, false, 512 << 10, 0, 4 << 20),
Some((4 << 20, LlcKind::SystemLevel))
);
assert_eq!(resolve_llc(None, false, 1 << 20, 0, 0), None);
assert_eq!(resolve_llc(None, false, 1 << 20, 1 << 20, 512 << 10), None);
assert_eq!(
resolve_llc(Some(0), false, 1 << 20, 4 << 20, 0),
Some((4 << 20, LlcKind::Dedicated))
);
}
#[test]
fn slc_probe_never_panics() {
let _ = system_level_cache_bytes();
let _ = last_level_cache();
}
#[test]
fn parse_cache_size_units() {
assert_eq!(parse_cache_size("512"), 512);
assert_eq!(parse_cache_size("256K"), 256 * 1024);
assert_eq!(parse_cache_size("8M"), 8 * 1024 * 1024);
assert_eq!(parse_cache_size(" 1024k "), 1024 * 1024);
assert_eq!(parse_cache_size("garbage"), 0);
}
#[test]
fn defaults_are_nonzero() {
let unknown = CacheInfo::default();
assert!(unknown.l1_data_or_default() >= 32 * 1024);
assert_eq!(unknown.l2_or_default(), 256 * 1024);
}
#[test]
fn detected_values_are_sane_when_present() {
let ci = cache_info();
for (name, v) in [("l1d", ci.l1_data), ("l2", ci.l2), ("l3", ci.l3)] {
assert!(v == 0 || (1024..=512 * 1024 * 1024).contains(&v), "{name} implausible: {v}");
}
if ci.l1_data > 0 && ci.l2 > 0 {
assert!(ci.l1_data <= ci.l2, "L1 {} > L2 {}", ci.l1_data, ci.l2);
}
if ci.l2 > 0 && ci.l3 > 0 {
assert!(ci.l2 <= ci.l3, "L2 {} > L3 {}", ci.l2, ci.l3);
}
}
}