Repository navigation
selector: fuse mul+add → mla — flat_flight filter emits separate mul+add where native uses MLA #257
Description
Activity
Shipped in v0.11.31: ArmOp::Mla + control-flow-sound fuse_mul_add wired into codegen. Measured flat_flight 1891→1819 B (~18 muls fused); flight_seam stays 0x07FDF307 with the fusion firing.
Heads-up: the MLA wiring shipped in v0.11.31, but on the actual gale
flat_flight(the named target of this issue) it fires zero times — I rebuilt on4c3e6f6and the object is byte-identical to pre-MLA:mul=4, mla=0, 170 instrs, still 255 cyc on the G474RE. The "~18 muls fused / 1891→1819 B" must be a simpler fixture; the deployed filter doesn't match the pattern. Here's the exact window:mul r5, r3, r4 ; gyro*980 (axis 1) add.w ip, r1, #8 ; address calc — NOT the consumer ldrsh.w r6, [fp, ip] ; load movw r7, #0x14 mul r8, r6, r7 ; accel*20 (axis 2) add.w r2, r5, r8 ; = gyro*980 + accel*20 ← consumes BOTH mulsTwo fusable pairs, neither fires:
mul r8,r6,r7;add.w r2, r5, r8— adjacent, should becomemla r2, r6, r7, r5(the mul resultr8is the add's second operand;r5is the accumulator). My guess: the pass matches the mul only as the add'srn(first operand), notop2— i.e.add-commutativity isn't handled. This is the cheap fix and it alone catches the real pattern.mul r5,r3,r4…add r2,r5,r8— consumer is 4 instrs later with anldrsh [fp,ip]between; the conservative "unmodeled instruction blocks fusion" rule likely trips on the load even though it can't touchr5/r3/r4. Looking through a non-aliasing load would catch this one too, but (1) is sufficient.
The scheduler interleaves the two filter axes (address-calc + load + the other axis's mul land between a mul and its add), so the immediately-adjacent case with the mul as
op2is the common shape in real code. Worth reopening — happy to re-measure the moment the commutative case fires (flat_flight-microbenchstaged, native floor is 103, currently 255). The op + encoder are correct; it's just the match condition that misses the deployed pattern.Correction to my earlier comment — I instrumented
fuse_mul_addand the real cause is not commutativity (the pass already tries the mul as both operands) nor the load (loads are modeled). Apologies for the wrong guess. The actual blocker, from aFUSE_DEBUGdump of the realflat_flightstream:[20] Mul R5,R3,R4 ; mul#1 (gyro*980) [23] Mul R8,R6,R7 ; mul#2 (accel*20) [24] Add R2,R5,R8 ; consumes R5+R8 — the fusable site [36] Mul R5,R3,R4 ; axis-2 REUSES R5 [37] Add R6,R2,R5 ; reads the *new* R5 [49] Add R3,R5,R8 ; reads the *new* R5used_elsewherescans the whole function for any read ofrmul(R5):let used_elsewhere = (0..n).any(|k| k != j && !removed[k] && op_may_use(&instrs[k].op, rmul));
It finds R5 read at [37]/[49] and bails — but those are reads of the redefined R5 (from [36]), not mul#1's value, which is dead right after [24]. So register reuse by the greedy selector blocks the fusion, on every axis. That's why a clean fixture (distinct regs) fuses ~18 muls but the deployed filter fuses zero.
The good news: this resolves itself under VCR-RA-001. Once the allocator emits non-reused virtual registers (SSA-style), each mul result has a unique vreg,
used_elsewheresees only the real consumer, and the fusion fires — so mla becomes a free rider on the allocator wiring (#272), not a separate fix. If you want it before then, the standalone fix is to makeused_elsewhere(andbetween_ok) live-range-bounded — stop the scan at the next def ofrmul(here [36]); a use afterrmulis redefined reads a different value and shouldn't block.flat_flight-microbenchis staged either way — I'll confirm the silicon delta when mla fires (via the allocator or the live-range bound).Fixed in v0.11.32 — the fusion now fires on the real
flat_flight.Your commutativity guess was a red herring: the pass already tries both operand orders. I traced
r8's live range on the actual--relocatableobject and found the real cause — theused_elsewheresoundness check scanned the whole function for any read of the mul-result register, but the single-pass allocator recyclesr8for unrelated later values:7e: mul.w r8, r6, r7 ; r8 = accel*20 ← our mul 82: add.w r2, r5, r8 ; consumes r8 ← the only real use a4: movw r8, #980 ; r8 REDEFINED to a different value a8: mul.w r2, r7, r8 ; reads the NEW r8The reads of the new
r8(ata8,e8, …) falsely blocked every fusion. Replaced the whole-function scan with a precise live-range check (dead-after-the-add-until-redefined).Measured on
flat_flight.loom.wasm(cortex-m4--relocatable):before after mul4 2 mla0 2 instructions 170 168 Both filter products (
gyro*980,accel*20) now fuse. All three differential fixtures result-identical —flight_seamstays 0x07FDF307 with the fusion firing. Whenever you reflash the G474RE off v0.11.32, the262 → ?cyc delta would close the loop. (Note this is the instruction-selection lever; the const-CSE clamp lever on #209 is still the bigger pending item.)- added a commit that references this issue
on Jun 5, 2026
Lever #2 from the flat_flight gap decomposition: fuse
mul+add→mlaPer the 262→103 gap decomposition, after const-CSE the next instruction-selection lever for
flat_flightis multiply-accumulate fusion. Measured on currentmain(f6a0c96, cortex-m4f):synth lowers the filter's
gyro*980 + accel*20as separatemulthenadd(2 sites — pitch + roll axes):native (gcc -O2) uses MLA — the add is free:
The transform
Peephole: when a
mul rD, rA, rBresult feeds exactly oneadd rE, rD, rC(rD not otherwise live), rewrite tomla rE, rA, rB, rCand drop themul. Cortex-M4 has single-cycleMLA. Saves 1 instruction + 1 temp register per site — 2 sites inflat_flight, and it recurs in anya*k1 + b*k2filter/accumulator (very common in control code).Bonus adjacent: multiply-by-constant strength reduction
The multipliers here are constants (980, 20). Native strength-reduces
*20toadd.w r,r,r,lsl#2; lsls #2(=*5 *4), avoiding themovw #20; mulentirely. Amul-by-small-constant → shift/add peephole would compound with the MLA fusion. (Lower priority than the MLA fold itself.)Scope
Pure instruction selection (no regalloc dependency), so it composes cleanly with the const-CSE/spill work on the VCR-RA-001 track.
flat_flight-microbench(261) +controller(168) are staged — I'll post the silicon delta when it lands. Filing per my offer on #209; close as dup/wontfix if it's already on the roadmap.