Skip to main content

padlock_core/analysis/
locality.rs

1// padlock-core/src/analysis/locality.rs
2
3use crate::ir::{AccessPattern, Field, StructLayout};
4
5pub struct FieldLocality<'a> {
6    pub field: &'a Field,
7    pub is_hot: bool,
8}
9
10fn is_hot(f: &Field) -> bool {
11    matches!(
12        f.access,
13        AccessPattern::ReadMostly | AccessPattern::Concurrent { .. }
14    )
15}
16
17/// Classify every field as hot or cold.
18pub fn classify_fields(layout: &StructLayout) -> Vec<FieldLocality<'_>> {
19    layout
20        .fields
21        .iter()
22        .map(|f| FieldLocality {
23            field: f,
24            is_hot: is_hot(f),
25        })
26        .collect()
27}
28
29/// Returns `true` if the layout has a hot/cold locality problem.
30///
31/// Two conditions are checked:
32///
33/// 1. **Interleaving** — a hot→cold→hot transition exists (classic case).
34/// 2. **Cache-line mixing** — when the struct spans more than one cache line,
35///    any cache line that contains both hot and cold fields is a problem even
36///    without interleaving.  Architectures with `cache_line_size == 0` (e.g.
37///    Cortex-M with no cache) skip check 2.
38pub fn has_locality_issue(layout: &StructLayout) -> bool {
39    let classified = classify_fields(layout);
40    let has_hot = classified.iter().any(|c| c.is_hot);
41    let has_cold = classified.iter().any(|c| !c.is_hot);
42    if !has_hot || !has_cold {
43        return false;
44    }
45
46    // Check 1: hot→cold→hot interleaving.
47    let mut saw_cold_after_hot = false;
48    let mut last_was_hot = false;
49    for c in &classified {
50        if c.is_hot {
51            if saw_cold_after_hot {
52                return true;
53            }
54            last_was_hot = true;
55        } else if last_was_hot {
56            saw_cold_after_hot = true;
57        }
58    }
59
60    // Check 2: hot and cold fields share a cache line when the struct spans
61    // multiple cache lines.  If the whole struct fits in one cache line it all
62    // gets loaded together anyway, so this check adds no signal.
63    let cl = layout.arch.cache_line_size;
64    if cl > 0 && layout.total_size > cl {
65        let mut line_has_hot = std::collections::HashMap::<usize, bool>::new();
66        let mut line_has_cold = std::collections::HashMap::<usize, bool>::new();
67        for c in &classified {
68            let line = c.field.offset / cl;
69            if c.is_hot {
70                line_has_hot.insert(line, true);
71            } else {
72                line_has_cold.insert(line, true);
73            }
74        }
75        if line_has_hot
76            .keys()
77            .any(|line| line_has_cold.contains_key(line))
78        {
79            return true;
80        }
81    }
82
83    false
84}
85
86/// Split fields into (hot_names, cold_names) preserving original order.
87pub fn partition_hot_cold(layout: &StructLayout) -> (Vec<String>, Vec<String>) {
88    let mut hot = Vec::new();
89    let mut cold = Vec::new();
90    for f in &layout.fields {
91        if is_hot(f) {
92            hot.push(f.name.clone());
93        } else {
94            cold.push(f.name.clone());
95        }
96    }
97    (hot, cold)
98}
99
100// ── tests ─────────────────────────────────────────────────────────────────────
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::arch::X86_64_SYSV;
106    use crate::ir::{Field, StructLayout, TypeInfo};
107
108    fn field(name: &str, offset: usize, access: AccessPattern) -> Field {
109        Field {
110            name: name.into(),
111            ty: TypeInfo::Primitive {
112                name: "u64".into(),
113                size: 8,
114                align: 8,
115            },
116            offset,
117            size: 8,
118            align: 8,
119            source_file: None,
120            source_line: None,
121            access,
122        }
123    }
124
125    fn layout(fields: Vec<Field>) -> StructLayout {
126        StructLayout {
127            name: "T".into(),
128            total_size: fields.len() * 8,
129            align: 8,
130            fields,
131            source_file: None,
132            source_line: None,
133            arch: &X86_64_SYSV,
134            is_packed: false,
135            is_union: false,
136            is_repr_rust: false,
137            suppressed_findings: Vec::new(),
138            uncertain_fields: Vec::new(),
139        }
140    }
141
142    #[test]
143    fn interleaved_hot_cold_is_issue() {
144        // hot cold hot — locality issue
145        let l = layout(vec![
146            field("a", 0, AccessPattern::ReadMostly),
147            field("b", 8, AccessPattern::Unknown),
148            field("c", 16, AccessPattern::ReadMostly),
149        ]);
150        assert!(has_locality_issue(&l));
151    }
152
153    #[test]
154    fn hot_first_then_cold_is_fine() {
155        let l = layout(vec![
156            field("a", 0, AccessPattern::ReadMostly),
157            field("b", 8, AccessPattern::ReadMostly),
158            field("c", 16, AccessPattern::Unknown),
159            field("d", 24, AccessPattern::Unknown),
160        ]);
161        assert!(!has_locality_issue(&l));
162    }
163
164    #[test]
165    fn all_unknown_no_issue() {
166        let l = layout(vec![
167            field("a", 0, AccessPattern::Unknown),
168            field("b", 8, AccessPattern::Unknown),
169        ]);
170        assert!(!has_locality_issue(&l));
171    }
172
173    #[test]
174    fn hot_then_cold_sharing_cache_line_is_issue() {
175        // 9 fields × 8B = 72B > 64B cache line.
176        // hot field at offset 0 and cold fields at offsets 8–63 all share cache
177        // line 0, even though the layout is hot-first (no interleaving).
178        let mut fields = vec![field("hot0", 0, AccessPattern::ReadMostly)];
179        for i in 1..9usize {
180            fields.push(field(&format!("cold{i}"), i * 8, AccessPattern::Unknown));
181        }
182        let l = layout(fields);
183        assert!(
184            has_locality_issue(&l),
185            "hot and cold sharing a cache line must be flagged"
186        );
187    }
188
189    #[test]
190    fn hot_and_cold_on_separate_cache_lines_is_fine() {
191        // All hot fields fit within the first cache line; all cold fields start
192        // on the second cache line.  No mixing → no issue.
193        // 8 hot fields × 8B = 64B exactly fills cache line 0.
194        let mut fields: Vec<Field> = (0usize..8)
195            .map(|i| field(&format!("hot{i}"), i * 8, AccessPattern::ReadMostly))
196            .collect();
197        // cold field starts at offset 64 — exactly cache line 1.
198        fields.push(field("cold0", 64, AccessPattern::Unknown));
199        // Manually set total_size to 72 (not fields.len()*8 which the helper sets).
200        let mut l = layout(fields);
201        l.total_size = 72;
202        assert!(
203            !has_locality_issue(&l),
204            "hot/cold on separate cache lines must not be flagged"
205        );
206    }
207
208    #[test]
209    fn hot_then_cold_within_one_cache_line_not_flagged() {
210        // Struct fits entirely within one 64B cache line — check 2 does not apply.
211        let l = layout(vec![
212            field("a", 0, AccessPattern::ReadMostly),
213            field("b", 8, AccessPattern::ReadMostly),
214            field("c", 16, AccessPattern::Unknown),
215            field("d", 24, AccessPattern::Unknown),
216        ]);
217        assert!(!has_locality_issue(&l));
218    }
219
220    #[test]
221    fn partition_separates_correctly() {
222        let l = layout(vec![
223            field("a", 0, AccessPattern::ReadMostly),
224            field("b", 8, AccessPattern::Unknown),
225            field(
226                "c",
227                16,
228                AccessPattern::Concurrent {
229                    guard: None,
230                    is_atomic: true,
231                    is_annotated: false,
232                },
233            ),
234        ]);
235        let (hot, cold) = partition_hot_cold(&l);
236        assert_eq!(hot, vec!["a", "c"]);
237        assert_eq!(cold, vec!["b"]);
238    }
239}