use crate::ir::{AccessPattern, Field, StructLayout};
pub struct FieldLocality<'a> {
pub field: &'a Field,
pub is_hot: bool,
}
fn is_hot(f: &Field) -> bool {
matches!(
f.access,
AccessPattern::ReadMostly | AccessPattern::Concurrent { .. }
)
}
pub fn classify_fields(layout: &StructLayout) -> Vec<FieldLocality<'_>> {
layout
.fields
.iter()
.map(|f| FieldLocality {
field: f,
is_hot: is_hot(f),
})
.collect()
}
pub fn has_locality_issue(layout: &StructLayout) -> bool {
let classified = classify_fields(layout);
let has_hot = classified.iter().any(|c| c.is_hot);
let has_cold = classified.iter().any(|c| !c.is_hot);
if !has_hot || !has_cold {
return false;
}
let mut saw_cold_after_hot = false;
let mut last_was_hot = false;
for c in &classified {
if c.is_hot {
if saw_cold_after_hot {
return true;
}
last_was_hot = true;
} else if last_was_hot {
saw_cold_after_hot = true;
}
}
let cl = layout.arch.cache_line_size;
if cl > 0 && layout.total_size > cl {
let mut line_has_hot = std::collections::HashMap::<usize, bool>::new();
let mut line_has_cold = std::collections::HashMap::<usize, bool>::new();
for c in &classified {
let line = c.field.offset / cl;
if c.is_hot {
line_has_hot.insert(line, true);
} else {
line_has_cold.insert(line, true);
}
}
if line_has_hot
.keys()
.any(|line| line_has_cold.contains_key(line))
{
return true;
}
}
false
}
pub fn partition_hot_cold(layout: &StructLayout) -> (Vec<String>, Vec<String>) {
let mut hot = Vec::new();
let mut cold = Vec::new();
for f in &layout.fields {
if is_hot(f) {
hot.push(f.name.clone());
} else {
cold.push(f.name.clone());
}
}
(hot, cold)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arch::X86_64_SYSV;
use crate::ir::{Field, StructLayout, TypeInfo};
fn field(name: &str, offset: usize, access: AccessPattern) -> Field {
Field {
name: name.into(),
ty: TypeInfo::Primitive {
name: "u64".into(),
size: 8,
align: 8,
},
offset,
size: 8,
align: 8,
source_file: None,
source_line: None,
access,
}
}
fn layout(fields: Vec<Field>) -> StructLayout {
StructLayout {
name: "T".into(),
total_size: fields.len() * 8,
align: 8,
fields,
source_file: None,
source_line: None,
arch: &X86_64_SYSV,
is_packed: false,
is_union: false,
is_repr_rust: false,
suppressed_findings: Vec::new(),
uncertain_fields: Vec::new(),
}
}
#[test]
fn interleaved_hot_cold_is_issue() {
let l = layout(vec![
field("a", 0, AccessPattern::ReadMostly),
field("b", 8, AccessPattern::Unknown),
field("c", 16, AccessPattern::ReadMostly),
]);
assert!(has_locality_issue(&l));
}
#[test]
fn hot_first_then_cold_is_fine() {
let l = layout(vec![
field("a", 0, AccessPattern::ReadMostly),
field("b", 8, AccessPattern::ReadMostly),
field("c", 16, AccessPattern::Unknown),
field("d", 24, AccessPattern::Unknown),
]);
assert!(!has_locality_issue(&l));
}
#[test]
fn all_unknown_no_issue() {
let l = layout(vec![
field("a", 0, AccessPattern::Unknown),
field("b", 8, AccessPattern::Unknown),
]);
assert!(!has_locality_issue(&l));
}
#[test]
fn hot_then_cold_sharing_cache_line_is_issue() {
let mut fields = vec![field("hot0", 0, AccessPattern::ReadMostly)];
for i in 1..9usize {
fields.push(field(&format!("cold{i}"), i * 8, AccessPattern::Unknown));
}
let l = layout(fields);
assert!(
has_locality_issue(&l),
"hot and cold sharing a cache line must be flagged"
);
}
#[test]
fn hot_and_cold_on_separate_cache_lines_is_fine() {
let mut fields: Vec<Field> = (0usize..8)
.map(|i| field(&format!("hot{i}"), i * 8, AccessPattern::ReadMostly))
.collect();
fields.push(field("cold0", 64, AccessPattern::Unknown));
let mut l = layout(fields);
l.total_size = 72;
assert!(
!has_locality_issue(&l),
"hot/cold on separate cache lines must not be flagged"
);
}
#[test]
fn hot_then_cold_within_one_cache_line_not_flagged() {
let l = layout(vec![
field("a", 0, AccessPattern::ReadMostly),
field("b", 8, AccessPattern::ReadMostly),
field("c", 16, AccessPattern::Unknown),
field("d", 24, AccessPattern::Unknown),
]);
assert!(!has_locality_issue(&l));
}
#[test]
fn partition_separates_correctly() {
let l = layout(vec![
field("a", 0, AccessPattern::ReadMostly),
field("b", 8, AccessPattern::Unknown),
field(
"c",
16,
AccessPattern::Concurrent {
guard: None,
is_atomic: true,
is_annotated: false,
},
),
]);
let (hot, cold) = partition_hot_cold(&l);
assert_eq!(hot, vec!["a", "c"]);
assert_eq!(cold, vec!["b"]);
}
}