1use 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
17pub 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
29pub 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 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 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
86pub 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#[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 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 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 let mut fields: Vec<Field> = (0usize..8)
195 .map(|i| field(&format!("hot{i}"), i * 8, AccessPattern::ReadMostly))
196 .collect();
197 fields.push(field("cold0", 64, AccessPattern::Unknown));
199 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 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}