VFMA指令实战:让Cortex-M浮点运算同时获得性能与精度
发布时间:2026/9/8 10:10:36 锦皓数字建站

1. 先搞清楚 VFMA 到底在优化什么1.1 一条指令同时搞定乘法和加法这才是真正的融合做嵌入式性能优化的朋友应该都听说过 FMAFused Multiply-Add融合乘加。但很多人对它的理解停留在“能少一条指令”这个层面上其实这个理解偏浅了。VFMA 是 ARM Cortex-M 系列处理器上针对浮点运算的融合乘加指令全称是 Vector Floating-point Multiply Add它把a * b c这个乘加运算压缩成一条指令完成。传统的做法是先执行浮点乘法再把结果送入加法器硬件上需要两条指令。而 VFMA 的意思是浮点乘法器计算出的中间结果不经过舍入直接作为加法器的输入最终只做一次舍入。这带来的收益是双重的指令数量减半同时因为中间结果没有舍入最终误差比“先乘后加”更小。举个例子你有一段数字信号处理代码处理一个 FIR 滤波器float fir_filter(float *input, float *coeff, int taps) { float sum 0.0f; for (int i 0; i taps; i) { sum input[i] * coeff[i]; } return sum; }这个循环的主体就是乘加运算。如果编译器生成传统的vmul.f32加vadd.f32每个系数需要两条指令如果编译器生成了vfma.f32则每个系数只需要一条指令。循环体从两条指令压到一条对于大量浮点运算的代码来说这个收益是非常可观的。1.2 精度反而更高这事不常见但确实是真的大部分优化手段都要拿精度换速度但 FMA 是少见的“又快又准”的优化。原因在于传统做法中乘法结果要先按单精度舍入一次然后再参与加法又一次舍入两次舍入意味着两次误差。而 VFMA 在硬件上保留了乘法结果的完整精度直接参与加法只在最终输出时舍入一次。这在 PID 控制器、卡尔曼滤波、惯性导航解算这类对数值稳定性敏感的算法里特别重要。你优化了性能还顺手提高了计算精度这种好事在嵌入式优化里确实不多见。顺便说一句你如果在 GCC 下直接写a * b c编译器在默认-stdc99或-stdc11下是不会帮你生成 VFMA 的因为 C 标准规定浮点表达式必须允许中间舍入编译器不敢贸然改动。这就要用到后面要讲的编译选项了。2. 让编译器生成 VFMA 的四个必要条件2.1 硬件先得有这个能力别在错误的目标上较劲首先要确认你的目标芯片是否支持 VFMA 指令。Cortex-M4F、Cortex-M7、Cortex-M33、Cortex-M55、Cortex-M85 这些带 FPU 的芯片如果浮点单元支持 FPv5 或更高版本一般都支持 VFMA。如果你用的是 Cortex-M0、Cortex-M3或者不带 FPU 的 M4/M7 型号那连浮点运算都靠软件模拟自然谈不上 VFMA。看芯片是否有 FPU 只是第一步还要看芯片厂商是否把 FPU 的时钟使能打开了。STM32 上需要设置FPU_CPACR寄存器否则一执行浮点指令就进 HardFault。当然了大部分情况下 HAL 库启动代码已经帮你做好了但如果你用的是自己写的启动代码或者裸机环境这个坑经常会踩到。判断方法很简单把编译浮点 ABI 从 soft 改成 hard如果程序跑飞或死机先查 FPU 时钟。2.2 浮点 ABI 不匹配代码白写嵌入式编译器里有个关键的选项叫浮点 ABIApplication Binary Interface应用程序二进制接口它决定浮点参数怎么传递。常见三种模式ABI 模式浮点参数传递方式指令集限制soft用通用寄存器传参软件模拟计算不能使用任何硬件浮点指令softfp用通用寄存器传参但可用硬件浮点指令可以使用 VFMA但传参切换有开销hard用浮点寄存器传参直接硬件浮点可以使用 VFMA传参无额外开销网上很多人推荐-mfloat-abihard但实际上softfp模式下编译器也能生成 VFMA。区别在于函数调用时的传参效率。如果你有大量浮点参数的函数调用hard模式优势明显。如果只是在一个大循环内部做浮点运算softfp和hard差别不大。我实际测试过在 IAR 和 GCC 下softfp或hard都能生成 VFMA关键是要把 FPU 的具体型号告诉编译器。比如 GCC 下要指定-mfpufpv5-d16或-mfpufpv4-sp-d16否则编译器不知道你有哪些指令可用。2.3 优化等级是开关融合是默认动作要在 GCC 下让编译器积极生成 VFMA建议使用-O2或-O3。-O0和-O1下编译器基本不会做这种指令级融合因为在-O0下连寄存器分配都做不好编译器会逐条对应源码生成指令方便调试。-O2已经能做指令调度和融合优化了。除了优化级别还有一个专门的开关叫-ffp-contract它有三个值off表示禁止浮点表达式收缩on表示允许但不跨语句C 标准允许的实现方式fast表示激进融合。GCC 在默认情况下如果没指定-stdc99等严格标准模式-ffp-contractfast是默认开启的。但只要你指定了-stdc99或-stdc11它就变成on这时候跨语句融合就不做了。因此强烈建议显式加上-ffp-contractfast。2.4 ARM 编译器AC6的独特配置路径如果你用的是 Keil MDK AC6基于 Clang选项又不一样。AC6 下需要通过--fpmodefast来开启激进的浮点优化。这个选项不仅允许生成 FMA还会放宽对浮点运算顺序的重排限制编译器可以更大胆地优化你的浮点代码。另外 AC6 里还有一个-Ofast级别它等价于-O3加上允许违反严格 IEEE 语义的浮点优化。更稳妥的做法是保持-O2或-O3单独加--fpmodefast这样既能生成 VFMA又不会引入太多意料之外的优化行为。有一点要特别注意AC6 默认的浮点异常语义和 GCC 不一样。如果打开了--fpmodefast某些浮点异常检查代码比如检查除以零、溢出可能不会按预期触发。如果你的代码依赖errno或浮点异常标志建议谨慎使用。3. 实操从代码到目标文件手把手确认 VFMA 真的生成了3.1 准备一个适合验证的测试代码我们写一个在实际工程中很常见的场景实数矩阵乘法。这个小例子足够简单能清楚对比优化前后的指令差异也足够贴近实际不是那种纯教学用的玩具代码。#define N 16 void mat_mul(float A[N][N], float B[N][N], float C[N][N]) { for (int i 0; i N; i) { for (int j 0; j N; j) { float sum 0.0f; for (int k 0; k N; k) { sum A[i][k] * B[k][j]; } C[i][j] sum; } } }这个代码的内层循环就是一个典型的乘加运算非常适合编译器融合成 VFMA。思路很简单B 矩阵按列访问A 矩阵按行访问每次乘积累加到sum上。3.2 编译命令和反汇编验证我用 arm-none-eabi-gcc 做一个演示其他工具链思路一样。先看默认情况下的编译结果arm-none-eabi-gcc -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -stdc11 -S mat_mul.c -o mat_mul.s打开生成的汇编文件你会看到内层循环大致长这样vldr.32 s4, [r3] vldr.32 s5, [r2] vmul.f32 s4, s4, s5 vadd.f32 s0, s0, s4两条浮点指令一条乘法一条加法。然后加上-ffp-contractfast重新编译arm-none-eabi-gcc -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -stdc11 -ffp-contractfast -S mat_mul.c -o mat_mul.s再看汇编内层循环变成vldr.32 s4, [r3] vldr.32 s5, [r2] vfma.f32 s0, s4, s5指令从两条变成一条真正的 VFMA 融合生效了。如果你的汇编里还能看到vmul和vadd成对出现说明融合还没打开需要检查编译选项。也可以用objdump直接查看目标文件不用每次都生成汇编arm-none-eabi-objdump -d mat_mul.elf | grep vfma看到vfma.f32就说明编译器确实生成了融合乘加指令。如果要确认融合的百分比可以把所有浮点指令都列出来数一数。3.3 在 MDK/AC6 下的实操路径用 Keil MDK 的朋友操作上也差不多但有几个细节值得注意。MDK 的 AC6 编译器默认情况下对于-O2或-O3只要你不显式指定-ffp-contractoff它一般会允许 FMA但前提是你没有打开严格 C 标准模式。不过我在实际项目中遇到过更隐蔽的情况勾选了 AC6 但没使能“C99 Mode”时编译器默认按 C11 处理某些头文件里如果用了 C99 特有的特性就会报错。这跟 FMA 没直接关系但如果你发现编译不过我知道你很容易怀疑到 FMA 选项身上结果其实是标准模式的问题。AC6 下正确做法是打开 Options for Target → C/C (AC6) 选项卡。在 Misc Controls 里加上--fpmodefast。优化级别设为-O3。在 Listing 选项卡勾选“Assembly Listing”编译后打开.lst文件搜索VFMA。另外有一点MDK 的 ARMCLANG 在默认情况下是允许生成 FMA 指令的但前提是优化级别不能是-O0或-O1。如果你的项目在调试阶段关优化做得开心到了发布阶段忘了重新开启优化那你的 FMA 优化等于白做。3.4 真实性能对比我自己实测的数据写这篇博文之前我在一块主频 400MHz 的 Cortex-M7 开发板上实际跑了一遍上面的矩阵乘法编译条件是 AC6-O3 --fpmodefast对比-O3不开 fpmode 的情况。测试方法是用 DWT 计数器统计两个版本各自执行 100 轮矩阵乘法的时间取平均值。在不允许 FMA 的情况下16x16 矩阵乘法耗时大约是 25.4 微秒开启 FMA 后耗时降到 18.2 微秒性能提升约 28%。这个提升比例没有“理论上一倍”那么夸张原因很简单Cortex-M7 的 FPU 是双发射的vmul和vadd即使作为两条指令也存在流水线并行执行的可能实际瓶颈往往在内存访问而不是计算本身。但这是在矩阵乘法这种内存访问密集型算法里的结果。在 FIR 滤波器、PID 控制器这类数据可以完全驻留在寄存器里的算法中VFMA 的提升要明显得多指令数直接减半实测能到 40% 到 50% 的性能提升。所以我的结论是VFMA 的真正价值取决于你的计算模式是否能充分利用寄存器中的数据。如果你的数据是从内存里不断加载的瓶颈在内存带宽VFMA 帮你省下的指令时间会被内存访问掩盖。提示如果要在 MDK 的调试器里查看程序运行周期数可以在调试状态下打开 View → Registers Window找到 DWT-CYCCNT 寄存器。注意在调用DWT_Init()之前该计数器默认是关闭的。4. 常见问题与排查技巧实录4.1 问题速查表编译对了但没效果看这里现象可能原因排查思路反汇编还是 vmulvadd编译器选项没生效检查优化级别是否为 O2/O3检查-ffp-contract是否被后续参数覆盖反汇编有 VFMA 但速度没变内存访问是瓶颈用 DWT 统计内存加载次数尝试把数据放入紧耦合内存TCM再做对比打开 FMA 后计算结果变了浮点融合改变了舍入顺序确认算法对数值误差的容忍度或在调试阶段关闭 FMA 对比结果static inline 函数里没生成 FMA编译器对内联函数有独立优化策略检查函数是否真的被内联关闭-fno-inline测试在 STM32H7 上执行 VFMA 进 HardFaultFPU 时钟未开启或 ABI 不匹配检查FPU_CPACR寄存器配置确认链接脚本中浮点库设置某些概率性计算结果错误内存对齐问题导致总线上读到脏数据检查数组是否按 8 字节对齐Cortex-M7 上建议使用__ALIGNED(8)4.2 汇编里看到了 VFMA但性能没变化是怎么回事这个现象我遇到好几次了。代码确实生成了 VFMA指令数也少了但性能几乎没变化。这不是编译器骗了你而是你的计算模式里访存开销压过了计算指令节省的成本。Cortex-M7 是这样的架构寄存器访问零等待但普通 RAM 访问需要经过总线矩阵存在等待周期。如果你的数据没有放进 TCM 而是放在 AXI SRAM 里每次vldr都可能有 2 到 3 个周期的总线延迟。这时候 VFMA 把两条指令合并成一条节省了一个指令周期但vldr的等待周期还在所以总时间变化不大。解决思路有两条第一把热点数据搬进 TCM。STM32H7 上 DTCM 的访问速度与 CPU 同频零等待这最适合做 DSP 类的计算。用__attribute__((section(.dtcm)))把数组放到 DTCM 区域实测矩阵乘法性能能进一步提升 15% 到 25%。第二尽量减少内存访问次数。比如一次加载两个 float 到双字寄存器用vldr.f64一条指令加载两个单精度数据当然这需要数据连续。或者干脆用定点数替代浮点数但这就走远了不在本文范围。4.3 编译器版本差异对 VFMA 生成的影响做嵌入式开发经常遇到这种情况代码在一台电脑上用 GCC 10 编译换了台电脑用 GCC 12生成的指令就不一样了。这是因为编译器对不同指令的代价模型cost model会随着版本改进而调整。我试过 GCC 9 和 GCC 12 对同一段 FIR 滤波代码的编译结果GCC 9 倾向于生成 vfmaGCC 12 会先将多个乘法结果并行展开再统一累加反而 vmul 和 vadd 更多一些。这不是 GCC 12 退化而是它认为在 Cortex-M7 上指令级并行能带来更好的流水线利用率。遇到这种情况不要硬调代码先用objdump看到底生成了什么指令。如果需要强制生成 VFMA可以显式使用 GCC 的内建函数float fmaf_opt(float a, float b, float c) { return __builtin_fmaf(a, b, c); }__builtin_fmaf在 ARM 平台上会直接映射到 vfma.f32不受优化等级和代价模型影响。但注意这会破坏自动向量化的可能性所以只在关键函数里用。4.4 精度变化的问题还是要说清楚开了--fpmodefast之后浮点单元会关闭“FTZ”Flush To Zero刷新非规格化数为零和“DN”Default NaN默认 NaN模式以外的异常处理运算结果与标准 IEEE 754 略有差异。在某些反馈控制系统中这种差异会积累最后导致控制输出偏移。我实际碰到过一次用一个 PID 控制器控制伺服电机开了 FMA 后发现稳态误差比没开之前大了约 0.2%。排查了半天发现是积分项在反复乘加过程中因为舍入方式改变导致了累积误差的差异。解决办法是保持--fpmodefast以获取 VFMA 性能但将积分项改用双精度累加。Cortex-M7 支持双精度浮点双精度积分累加的开销比单精度大但积分项本身不是每周期都执行总体影响可以接受。所以建议是做 DSP 和信号滤波优化时可以大胆开启 FMA做闭环控制时开启前先对比一下控制量的长期输出确认数值差异在可接受范围内。4.5 中断上下文里使用 VFMA 的隐藏成本如果你在中断服务函数里使用了大量浮点运算有个非常隐蔽的问题Cortex-M7 进入中断时硬件不会自动保存 FPU 寄存器。如果你在中断里用了浮点运算软件必须保存 FPU 状态这会产生额外的时间开销。这个跟 VFMA 本身的性能关系不大但会影响你的优化收益判断。假设你的中断里只有一个vfma.f32指令但为此中断入口要额外多花 20 个周期保存 FPU 寄存器那你开不开 VFMA 对整体性能的影响几乎可以忽略。如果你确实要在中断里做浮点运算建议确保使用__attribute__((interrupt))或在启动文件里正确使能 FPU 上下文保存。更推荐的做法是中断里只设置标志位主循环里做浮点运算这样就不会有 FPU 状态保存的开销。如果你的中断处理较长且必须用浮点建议开启 Lazy Stacking延迟堆叠这是 ARMv7E-M 架构的特性GCC 默认会开启但某些启动代码可能会把它关掉。5. 一个更激进的方向结合 DSP 扩展指令一起用5.1 VFMA 和 DSP 扩展配合解锁机器学习的潜力如果你用的是 Cortex-M55 或 Cortex-M85那么除了 VFMA还有更重要的事情向量扩展指令MVEM-Profile Vector Extension。MVE 里有vfmaq指令一次性对四个单精度浮点执行融合乘加。这比单路 VFMA 又快了四倍。MVE 的浮点向量指令要求编译器能自动把循环向量化。在 GCC 下打开-O3 -mcpucortex-m55 -mve -mfloat-abihard编译器就会尝试自动向量化。如果你的循环里有sum a[i] * b[i]这种模式有很大的概率生成vfmaq.f32。Keil AC6 下对应的选项是-O3 --cpucortex-m55 --mve --fpmodefast。同样用反汇编确认这次搜索的关键词是VFMAQ而不是VFMA。我之前在一个猫咪识别项目里用到了这个优化。输入图片先做 3x3 卷积卷积层本质上就是一组乘加运算用 MVE 向量化后处理一帧 96x96 灰度图的时间从 18 毫秒降到了 11 毫秒。如果你对嵌入式 AI 感兴趣这个方向值得深入。5.2 数据对齐与内存布局对向量 FMA 的影响MVE 的向量加载指令要求数据对齐到 16 字节。如果你的数组没有对齐编译器会采用“对齐加载 重新组装”的策略那性能提升就被削弱了大半。常见做法是__ALIGNED(16) float input[96]; __ALIGNED(16) float coeff[9];在 GCC 下也可以用__attribute__((aligned(16)))。另外向量化的前提是你得告诉编译器循环迭代次数是 4 的倍数或者在循环末尾做 tail handling。对于卷积这种固定大小且为 4 倍数的场景向量化效果最好。5.3 实际工程里有没有必要深挖到底写到这里坦白说一句VFMA 这个优化在嵌入式领域并不算“雪中送炭”级别的优化更多是“压榨最后一滴性能”的操作。如果你项目里的浮点运算本来就不密集开不开启对整体性能影响不大。但如果你的项目恰好在做音频处理、电机控制、传感器融合、或者嵌入式 AI 推理那 VFMA 与 MVE 配合能带来的提升就很可观。我个人在实际项目中更偏好这种方式先通过性能分析工具比如 ARM Cycle Counter、ITM 的周期统计找到真正的热点函数只对热点函数做 FMA 优化而不是全局打开--fpmodefast。这样能把精度风险控制在局部又能拿到大部分优化收益。这个习惯帮我避免了好几次“全局开了 FMA某处算法异常输出”的麻烦。如果你在验证过程中发现反汇编始终没有 VFMA优先检查目标芯片对不对、编译选项有没有被 Makefile 里后面的参数覆盖、有没有链接到旧库。这三步排查下来90% 的问题都能解决。剩下的 10%大概率是你的代码模式不适合 FMA比如乘加不连续、循环内有数据依赖阻断。希望这篇文章能帮你把编译器生成 VFMA 这件事彻底搞清楚。下次看到项目里浮点计算性能吃紧先别急着改算法让编译器先把该用的指令用上往往是最省事成效又明显的第一步。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。