1use std::collections::HashMap;
7
8use nxpu_ir::{Arena, BinaryOp, Expression, Function, Handle, MathFunction, Module};
9
10use crate::Pass;
11use crate::dce::expression_operands;
12
13#[derive(Debug)]
15pub struct FmaFusion;
16
17impl Pass for FmaFusion {
18 fn name(&self) -> &str {
19 "fma-fusion"
20 }
21
22 fn run(&self, module: &mut Module) -> bool {
23 let mut changed = false;
24 for (_, func) in module.functions.iter_mut() {
25 changed |= run_on_function(func);
26 }
27 for ep in &mut module.entry_points {
28 changed |= run_on_function(&mut ep.function);
29 }
30 changed
31 }
32}
33
34fn build_ref_counts(arena: &Arena<Expression>) -> HashMap<Handle<Expression>, usize> {
36 let mut counts: HashMap<Handle<Expression>, usize> = HashMap::new();
37 for (_, expr) in arena.iter() {
38 for operand in expression_operands(expr) {
39 *counts.entry(operand).or_insert(0) += 1;
40 }
41 }
42 counts
43}
44
45fn run_on_function(func: &mut Function) -> bool {
46 let mut changed = false;
47
48 let ref_counts = build_ref_counts(&func.expressions);
49 let handles: Vec<Handle<Expression>> = func.expressions.iter().map(|(h, _)| h).collect();
50
51 for handle in handles {
52 let replacement = match &func.expressions[handle] {
53 Expression::Binary {
54 op: BinaryOp::Add,
55 left,
56 right,
57 } => try_fuse_fma(&func.expressions, *left, *right, &ref_counts),
58 _ => None,
59 };
60
61 if let Some(new_expr) = replacement {
62 func.expressions[handle] = new_expr;
63 changed = true;
64 }
65 }
66
67 changed
68}
69
70fn try_fuse_fma(
79 exprs: &Arena<Expression>,
80 left: Handle<Expression>,
81 right: Handle<Expression>,
82 ref_counts: &HashMap<Handle<Expression>, usize>,
83) -> Option<Expression> {
84 if let Expression::Binary {
86 op: BinaryOp::Multiply,
87 left: a,
88 right: b,
89 } = &exprs[left]
90 {
91 if ref_counts.get(&left).copied().unwrap_or(0) > 1 {
92 return None;
93 }
94 return Some(Expression::Math {
95 fun: MathFunction::Fma,
96 arg: *a,
97 arg1: Some(*b),
98 arg2: Some(right),
99 arg3: None,
100 });
101 }
102
103 if let Expression::Binary {
105 op: BinaryOp::Multiply,
106 left: a,
107 right: b,
108 } = &exprs[right]
109 {
110 if ref_counts.get(&right).copied().unwrap_or(0) > 1 {
111 return None;
112 }
113 return Some(Expression::Math {
114 fun: MathFunction::Fma,
115 arg: *a,
116 arg1: Some(*b),
117 arg2: Some(left),
118 arg3: None,
119 });
120 }
121
122 None
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use nxpu_ir::Literal;
129
130 #[test]
131 fn fuse_mul_add() {
132 let mut func = Function::new("test");
133 let a = func
134 .expressions
135 .append(Expression::Literal(Literal::F32(2.0)));
136 let b = func
137 .expressions
138 .append(Expression::Literal(Literal::F32(3.0)));
139 let c = func
140 .expressions
141 .append(Expression::Literal(Literal::F32(4.0)));
142 let mul = func.expressions.append(Expression::Binary {
143 op: BinaryOp::Multiply,
144 left: a,
145 right: b,
146 });
147 let add = func.expressions.append(Expression::Binary {
148 op: BinaryOp::Add,
149 left: mul,
150 right: c,
151 });
152
153 let changed = run_on_function(&mut func);
154 assert!(changed);
155 match &func.expressions[add] {
156 Expression::Math {
157 fun: MathFunction::Fma,
158 arg,
159 arg1,
160 arg2,
161 arg3,
162 } => {
163 assert_eq!(*arg, a);
164 assert_eq!(*arg1, Some(b));
165 assert_eq!(*arg2, Some(c));
166 assert!(arg3.is_none());
167 }
168 other => panic!("expected Fma, got {other:?}"),
169 }
170 }
171
172 #[test]
173 fn fuse_commuted_add_mul() {
174 let mut func = Function::new("test");
176 let a = func
177 .expressions
178 .append(Expression::Literal(Literal::F32(2.0)));
179 let b = func
180 .expressions
181 .append(Expression::Literal(Literal::F32(3.0)));
182 let c = func
183 .expressions
184 .append(Expression::Literal(Literal::F32(4.0)));
185 let mul = func.expressions.append(Expression::Binary {
186 op: BinaryOp::Multiply,
187 left: a,
188 right: b,
189 });
190 let add = func.expressions.append(Expression::Binary {
191 op: BinaryOp::Add,
192 left: c,
193 right: mul,
194 });
195
196 let changed = run_on_function(&mut func);
197 assert!(changed);
198 match &func.expressions[add] {
199 Expression::Math {
200 fun: MathFunction::Fma,
201 arg,
202 arg1,
203 arg2,
204 arg3,
205 } => {
206 assert_eq!(*arg, a);
207 assert_eq!(*arg1, Some(b));
208 assert_eq!(*arg2, Some(c));
209 assert!(arg3.is_none());
210 }
211 other => panic!("expected Fma, got {other:?}"),
212 }
213 }
214
215 #[test]
216 fn no_fusion_without_multiply() {
217 let mut func = Function::new("test");
218 let a = func
219 .expressions
220 .append(Expression::Literal(Literal::F32(1.0)));
221 let b = func
222 .expressions
223 .append(Expression::Literal(Literal::F32(2.0)));
224 let _add = func.expressions.append(Expression::Binary {
225 op: BinaryOp::Add,
226 left: a,
227 right: b,
228 });
229
230 let changed = run_on_function(&mut func);
231 assert!(!changed);
232 }
233
234 #[test]
235 fn no_fusion_on_subtract() {
236 let mut func = Function::new("test");
237 let a = func
238 .expressions
239 .append(Expression::Literal(Literal::F32(2.0)));
240 let b = func
241 .expressions
242 .append(Expression::Literal(Literal::F32(3.0)));
243 let c = func
244 .expressions
245 .append(Expression::Literal(Literal::F32(4.0)));
246 let mul = func.expressions.append(Expression::Binary {
247 op: BinaryOp::Multiply,
248 left: a,
249 right: b,
250 });
251 let _sub = func.expressions.append(Expression::Binary {
252 op: BinaryOp::Subtract,
253 left: mul,
254 right: c,
255 });
256
257 let changed = run_on_function(&mut func);
258 assert!(!changed);
259 }
260
261 #[test]
262 fn no_fusion_multi_use_multiply() {
263 let mut func = Function::new("test");
265 let a = func
266 .expressions
267 .append(Expression::Literal(Literal::F32(2.0)));
268 let b = func
269 .expressions
270 .append(Expression::Literal(Literal::F32(3.0)));
271 let c = func
272 .expressions
273 .append(Expression::Literal(Literal::F32(4.0)));
274 let d = func
275 .expressions
276 .append(Expression::Literal(Literal::F32(5.0)));
277 let mul = func.expressions.append(Expression::Binary {
278 op: BinaryOp::Multiply,
279 left: a,
280 right: b,
281 });
282 let _add1 = func.expressions.append(Expression::Binary {
284 op: BinaryOp::Add,
285 left: mul,
286 right: c,
287 });
288 let _add2 = func.expressions.append(Expression::Binary {
289 op: BinaryOp::Add,
290 left: mul,
291 right: d,
292 });
293
294 let changed = run_on_function(&mut func);
295 assert!(!changed);
296 }
297
298 #[test]
299 fn fusion_single_use_multiply() {
300 let mut func = Function::new("test");
302 let a = func
303 .expressions
304 .append(Expression::Literal(Literal::F32(2.0)));
305 let b = func
306 .expressions
307 .append(Expression::Literal(Literal::F32(3.0)));
308 let c = func
309 .expressions
310 .append(Expression::Literal(Literal::F32(4.0)));
311 let mul = func.expressions.append(Expression::Binary {
312 op: BinaryOp::Multiply,
313 left: a,
314 right: b,
315 });
316 let add = func.expressions.append(Expression::Binary {
317 op: BinaryOp::Add,
318 left: mul,
319 right: c,
320 });
321
322 let changed = run_on_function(&mut func);
323 assert!(changed);
324 assert!(matches!(
325 &func.expressions[add],
326 Expression::Math {
327 fun: MathFunction::Fma,
328 ..
329 }
330 ));
331 }
332}