Build1 publisher3 min readPublished
One fmla in eleven instructions keeps a NEON matmul at 7% of the M2's 112 GFlops
A dev.to post takes a single-threaded f32 matmul from 2.09 to 7.86 GFlops in hand-written NEON on a MacBook Air M2. The inner loop runs eleven instructions for every one multiply.
The Engineer · Build desk

What happened
- The post fixes its target first: 112 GFlops of single-threaded f32 matmul on an M2, from 3.5 GHz, four FMA per cycle, four 32-bit lanes and two flops per FMA.
- A naive triple-loop C reference compiled at -O2 measured 2.09 GFlops, which the author puts at about 1.9 percent of that ceiling.
- Switching to fmla, with four B values in one register and one broadcast A value, reached 7.86 GFlops, about 7 percent of the ceiling and roughly four times the C baseline.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The 7.86 GFlops kernel only accepts N divisible by four, and the author skipped the overhang handling, so anyone lifting the code writes the tail path themselves.
- decision The 3.8x came from one instruction change; the remaining factor of 14 sits in unrolling, register blocking and packing. A team pricing a NEON kernel should budget for that work.
- contradiction The stated motivation is ARM in the cloud and the measurement is one MacBook Air, so the 112 GFlops denominator has to be recalculated per core before any of these percentages apply to a cloud instance.
The 112 GFlops target describes a loop that does almost nothing except multiply. One fmla on a 128-bit register performs four lanes of multiply-accumulate, so eight flops per instruction [2]. Four fmla per cycle is 32 flops per cycle, and at 3.5 GHz that comes to 112 GFlops [1]. Reaching it means four fmla issue every cycle, with their operands already in registers.
Compare that to the kernel that measured 7.86 GFlops [9]. At eight flops per fmla, 7.86 GFlops is about 0.98 billion fmla per second, one every 3.6 cycles at 3.5 GHz [4]. The published inner loop over k is eleven instructions: a compare and a branch, two madd to compute the A and B offsets, a shift and an add to turn one offset into a byte address, a ldr q for four floats of B, a ld1r to broadcast one float of A, the fmla, then the k increment and the loop branch [10]. Eleven instructions in 3.6 cycles is roughly three instructions retired per cycle [5].
There is a dependency problem on top of that. The register v2 is the only accumulator, cleared once before the k loop, and each fmla reads back the value the previous one wrote [11]. The author's next kernel opens with four accumulators and the comment "We are using 4 here to remove dependencies within K blocks" [12], pulls four consecutive A values with a post-incrementing ld1r, and advances the B offset by N shifted left two between four q loads [14]. That structure spreads the two madd computations across four fmla instead of one. The excerpt ends mid-instruction inside that kernel [18].
The scalar assembly result is the one I would keep. Writing the same loop by hand produced 1.98 GFlops against the compiler's 2.09 at -O2, about 5 percent slower [5][4][6], and the author wrote "the c compiler is smarter than me." [6] The 3.8x that followed came from switching the instruction [7].
Memory access is the other half of the gap. B is addressed at offset (k * N + n) [15], so each step of k walks forward N floats. At the top of the sweep N is 1024 [3], a 4 KB jump per step and 4 MB of B touched per pass [8]. Neither kernel packs or blocks B, and the figure quoted for each one is a single average across the whole sweep from 64x64 to 1024x1024 [3]. That average covers the shapes that fit in cache and the ones that do not.
The post explains its motivation with "and yet people keep making all these dang ARM computers in my cloud" [16], and then measures a MacBook Air with an M2 [1]. It does not benchmark a cloud instance. For these percentages to carry over you need a core at the same 3.5 GHz with four FMA pipes and 128-bit vectors [2]; move any of the three and the denominator moves with it. The author says the next article will use SME/SVE2 [17].
What to watch
- The GFlops figure for the four-accumulator unrolled kernel, which the published excerpt cuts off before reporting.
- The promised SME/SVE2 follow-up, where the ceiling calculation starts from different multiply hardware than the 128-bit NEON path.
- A per-shape breakdown instead of one average from 64x64 to 1024x1024, which would separate the cache-resident shapes from the 4 MB ones.