use crate::{
object::{attributes::ObjectAttributes, types::ObjectType},
topology::Topology,
};
use arrayvec::ArrayVec;
#[allow(unused)]
#[cfg(test)]
use similar_asserts::assert_eq;
impl Topology {
pub fn cpu_cache_stats(&self) -> Option<CpuCacheStats> {
CpuCacheStats::new(self)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CpuCacheStats {
smallest_data_cache_sizes: ArrayVec<u64, { DATA_CACHE_LEVELS.len() }>,
smallest_data_cache_sizes_per_thread: ArrayVec<u64, { DATA_CACHE_LEVELS.len() }>,
total_data_cache_sizes: ArrayVec<u64, { DATA_CACHE_LEVELS.len() }>,
}
impl CpuCacheStats {
pub fn new(topology: &Topology) -> Option<Self> {
let mut smallest_data_cache_sizes = ArrayVec::new();
let mut smallest_data_cache_sizes_per_thread = ArrayVec::new();
let mut total_data_cache_sizes = ArrayVec::new();
for &data_cache_level in DATA_CACHE_LEVELS {
let mut smallest = u64::MAX;
let mut smallest_per_thread = u64::MAX;
let mut total = 0;
let mut found = false;
for object in topology.objects_with_type(data_cache_level) {
let Some(ObjectAttributes::Cache(cache)) = object.attributes() else {
unreachable!("Caches should have cache attributes")
};
found = true;
let num_threads = object
.cpuset()
.and_then(|set| set.weight())
.expect("Caches should have cpusets") as u64;
let cache_size = cache.size()?.get();
let per_thread_size = cache_size / num_threads;
smallest = smallest.min(cache_size);
smallest_per_thread = smallest_per_thread.min(per_thread_size);
total += cache_size;
}
if found {
smallest_data_cache_sizes.push(smallest);
smallest_data_cache_sizes_per_thread.push(smallest_per_thread);
total_data_cache_sizes.push(total);
} else {
break;
}
}
(!smallest_data_cache_sizes.is_empty()).then_some(Self {
smallest_data_cache_sizes,
smallest_data_cache_sizes_per_thread,
total_data_cache_sizes,
})
}
pub fn smallest_data_cache_sizes(&self) -> &[u64] {
&self.smallest_data_cache_sizes[..]
}
pub fn smallest_data_cache_sizes_per_thread(&self) -> &[u64] {
&self.smallest_data_cache_sizes_per_thread[..]
}
pub fn total_data_cache_sizes(&self) -> &[u64] {
&self.total_data_cache_sizes[..]
}
}
const DATA_CACHE_LEVELS: &[ObjectType] = &[
ObjectType::L1Cache,
ObjectType::L2Cache,
ObjectType::L3Cache,
ObjectType::L4Cache,
ObjectType::L5Cache,
];
#[cfg(test)]
mod tests {
use super::*;
use similar_asserts::assert_eq;
#[test]
fn stats() {
let topology = Topology::test_instance();
let stats = topology.cpu_cache_stats();
if let Some(stats) = stats {
let num_cache_levels = stats.smallest_data_cache_sizes().len();
assert_eq!(
num_cache_levels,
stats.smallest_data_cache_sizes_per_thread().len()
);
assert_eq!(num_cache_levels, stats.total_data_cache_sizes().len());
assert!(num_cache_levels <= DATA_CACHE_LEVELS.len());
for (idx, cache_level) in DATA_CACHE_LEVELS.iter().copied().enumerate() {
if idx >= num_cache_levels {
assert_eq!(topology.objects_with_type(cache_level).count(), 0);
continue;
}
let cache_sizes_and_arities = topology.objects_with_type(cache_level).map(|obj| {
let arity = obj.cpuset().unwrap().weight().unwrap();
let Some(ObjectAttributes::Cache(cache)) = obj.attributes() else {
unreachable!()
};
(cache.size().unwrap().get(), arity)
});
assert_eq!(
stats.smallest_data_cache_sizes()[idx],
cache_sizes_and_arities
.clone()
.map(|(size, _)| size)
.min()
.unwrap()
);
assert_eq!(
stats.smallest_data_cache_sizes_per_thread()[idx],
cache_sizes_and_arities
.clone()
.map(|(size, arity)| size / arity as u64)
.min()
.unwrap()
);
assert_eq!(
stats.total_data_cache_sizes()[idx],
cache_sizes_and_arities
.clone()
.map(|(size, _)| size)
.sum::<u64>()
);
}
} else {
if topology.objects_with_type(ObjectType::L1Cache).count() == 0 {
return;
}
for &cache_level in DATA_CACHE_LEVELS {
for obj in topology.objects_with_type(cache_level) {
let Some(ObjectAttributes::Cache(cache)) = obj.attributes() else {
unreachable!()
};
if cache.size().is_none() {
return;
}
}
}
panic!("Cache stats query should not have failed");
}
}
}