ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ARM Cortex-M平台VFMA融合乘加指令优化:编译器配置与性能提升指南

ARM Cortex-M平台VFMA融合乘加指令优化:编译器配置与性能提升指南 写这篇东西之前我一直在犹豫要不要把题目起得这么窄。“VFMA融合乘加指令”听起来像是CPU架构教材里的一个术语离实际工程有点远。但如果你在Cortex-M4/M7/M33上写过浮点算法在STM32H743上调过FFT或者PID你大概率遇到过这样的场景明明计算量没变主频也够高但CoreMark分数就是上不去或者示波器里控制环的抖动总是多了那么几个微秒。这时候你会下意识打开反汇编窗口逐行检查然后很可能看到编译器没有生成你期望的指令序列。这篇就专门说一件事怎么让Keil MDK、IAR或者GCC在ARM Cortex-M平台上把a a b * c这种乘加组合编译成一条VFMA指令而不是先VMLA再VADD或者干脆生成一堆LDR、VLDR、VMOV的搬运指令。更重要的是我会把为什么编译器不生成、什么情况下它会生成、以及当你真的拿到VFMA之后数值上会发生什么变化这三件事讲透。无论你是刚把STM32F103项目迁移到H7系列还是在用Cortex-M33做马达控制这篇文章应该能帮你省下半天到一天的时间去和编译器“搏斗”。1. VFMA到底是个什么东西为什么值得折腾1.1 一条指令完成两件事乘和加VFMA是ARM的NEON/VFP指令集中的一条全称是Fused Multiply-Add。它在浮点流水线里执行的是d a b * c而且关键点在于中间结果b*c的舍入只在最后做一次。普通方式下b*c先算一次、舍入一次然后a再算一次、再舍入一次。VFMA从硬件层面把两步合成一步只做一次舍入。这个“一次舍入”的意义往小了说是精度更高往大了说是省了一条指令、省了一次寄存器搬运、省了一次流水线停顿。在Cortex-M7这种双发射、7级流水线的核心上省下的不只是1个时钟周期而是可能让整段浮点代码的发射宽度都变得更宽松从而让后面的load/store指令更早进入流水线。如果你觉得“一次舍入”太抽象可以类比成你做饭普通做法是先把菜炒熟盛盘再把汁淋上去两个步骤两次调味VFMA是一次性在锅里把汁和菜拌匀了再出锅调料融合得好还少洗一个盘子。盘子就是寄存器洗盘子就是寄存器访问开销。1.2 为什么嵌入式开发者特别需要关注它MCU上的资源是寸土寸金的。就拿Cortex-M7的FPU来说虽然它是双精度硬件浮点但浮点寄存器和通用寄存器之间的搬运路径以及VFP流水线和主流水线之间的互锁都比你想的要脆弱。如果你的代码写了大量浮点乘加编译器却没生成VFMA你可能会看到一条vadd.f32和一条vmul.f32再加上因为依赖关系导致的流水线停顿实际执行时间比理论时钟周期多出好几倍。尤其是跑FreeRTOS CMSIS-DSP这种组合的时候——DSP库内部已经是手工汇编优化但你自己的应用层代码往往是瓶颈。此时编译器是否能生成VFMA直接决定你的PID环路是1kHz还是1.5kHz决定你的传感器融合是200Hz还是320Hz。这不是“抠门”级的优化这是实打实的系统级收益。2. 编译器为什么有时候不生成VFMA2.1 语义约束IEEE 754和“严格浮点”规则这是最容易踩、也最容易忽略的坑。VFMA的“一次舍入”特性本质上是违反了IEEE 754标准里关于“中间结果也必须舍入”的规定。在严格的IEEE语义下表达式a a b * c必须先生成舍入后的b*c再相加、再舍入。如果你写的代码被编译器认定为“需要严格遵循IEEE 754语义”那么编译器就算手里有VFMA也不能用它否则会改变你程序的计算结果——虽然是更精确了但结果和严格按IEEE步骤算出来的不一样。这就是为什么很多默认配置下编译器不生成VFMA的根本原因不是编译器不聪明而是它没被授权去“作弊”变得更精确。Keil MDK里的AC5编译器armcc默认浮点语义是“标准兼容但允许有限优化”它在大多数情况下会生成VFMA所以很多老工程师觉得“AC5挺好的自动就有VFMA”。而AC6armclang不一样它默认按照Clang/LLVM的严格语义来在没有明确指定“放宽浮点语义”的情况下经常生成独立的VMLA或者VMULVADD序列。这就是网上大量“Keil AC6编译出来的浮点代码比AC5慢”说法的来源之一。2.2 编译器版本和优化选项差异GCC也类似。很多在GCC上做过嵌入式开发的人知道-ffp-contractfast是GCC默认的行为在-O2以上所以GCC的浮点乘加通常能自动合成FMA。但这里有个大坑Cortex-M3/M0/M0根本没有FPUGCC即便开了-mfpu也不会生成VFMA因为没有硬件指令可用。而Cortex-M4/M7/M33有FPU且支持VFPv4单精度/双精度理论上可以生成VFMA——前提是你得把FPU选项打开并确保架构级别允许。在不同编译器下的选项对照我后来整理成了一个表放在下面编译器主要选项说明armcc (AC5)--fpmodefastAC5默认在C90模式下就能生成VFMA但若用了--fpmodeieee则不会armclang (AC6)-ffp-contractfast或-ffast-math默认是on仅函数内合并但实际经验是配合-O2/-O3才稳定生成VFMAGCC-ffp-contractfastGCC默认就是fast但需确认-mfpufpv5-d16或fpv4-sp-d16等一系列FPU选项正确IAR--fp_contractfast或--fpuvfpv4IAR的默认比较激进但对严格语义支持也较好注意armclang的-ffp-contractfast是“可以在表达式内合并所有乘加”而不仅仅是当前函数内的连续乘加。这个选项在armclang 6.14之后能稳定地触发VFMA生成。2.3 芯片型号和FPU功能限制Cortex-M7核同时支持单精度和双精度浮点。但要注意STM32H7系列的不同型号Cortex-M7的FPU可能是双精度也可能是单精度。比如STM32H743/H753是双精度FPU而STM32H730/H750虽然是M7内核但部分型号的FPU配置不同。如果你的语法是正确的但编译器始终不生成VFMA检查一下你选的设备型号是否正确FPU选项是否选了Single precision而不是Double precision。在Keil里这个选项在Options → Target → Floating Point Hardware里一旦选错编译器会老老实实地用软件浮点库函数__aeabi_fmul这时候别说VFMA连VADD都不会有。同样Cortex-M33一般带FPU可选配置为单精度FPv5如果你的芯片选错了不带FPU的型号或者链接脚本里的启动文件没有启用FPU访问比如CPACR寄存器没配好那么就算编译器生成了VFMA跑起来也会因为UsageFault而死机。这个问题很隐蔽因为编译和链接都正常只有运行时报错。3. 怎么确认你已经拿到了VFMA3.1 反汇编窗口和map文件的配合在Keil里编译完进入Debug模式然后进View → Disassembly Window找到你关心的函数逐条查看生成的汇编指令。VFMA在Cortex-M7上反汇编长这样0x08000234 ED801A02 vfma.f32 s0, s1, s2注意最后一个操作码是vfma后缀.f32代表单精度。如果是双精度会显示vfma.f64。在GCC工具链下用objdump -d -S your_elf.elf也可以看到类似的输出。如果你看到的是vmul.f32后面紧跟着vadd.f32那说明编译器没合成VFMA。有一个细节容易忽略armclang在开启优化后可能会把变量优化到寄存器里反汇编里看不到任何存储指令这是正常的。你需要在反汇编窗口里搜索vfma、vmla另一个乘加指令语义类似但舍入方式不同和vadd这些关键字。如果是大项目直接CtrlF搜索比你肉眼一行一行扫快得多。3.2 从map文件里看编译器版本和优化级别map文件虽然不直接显示汇编指令但你可以从map文件头部看到编译器版本和命令行选项。比如Compiler: GNU GCC for ARM Embedded Processors 10.3.1 Command: arm-none-eabi-gcc -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -ffp-contractfast如果你发现命令行里-ffp-contract根本没出现那GCC默认也会是fast但保险起见显式写出来更好。而如果看到-mfloat-abisoftfp那浮点参数是通过整数寄存器传递的这也会降低性能但不会阻止VFMA生成。softfp和hard的区别在于调用约定不影响是否生成FMA指令但如果你用了-mfloat-abisoft那编译器就完全不会用FPU指令彻底没有VFMA。3.3 一个快速确认的暴力方法如果你懒得看反汇编还有一个更暴力的方法直接写一个函数用编译器的汇编输出功能。float test_fma(float a, float b, float c, float d) { return a * b c * d; }编译后看汇编输出。如果生成了一条VFMA说明编译器配置没问题如果生成两条VMUL和一条VADD说明要么优化级别太低要么浮点语义选项不对。这个最小用例比在几千行的大函数里找VFMA要快得多特别适合怀疑编译器配置有问题时做交叉验证。4. 实操让三个主流编译器乖乖生成VFMA4.1 Keil MDK中AC6的配置方法与坑先说AC6armclang。在Keil MDK 5.36以上版本里AC6已经成为默认编译器很多人项目一路上来没改过配置但它默认不开-ffp-contractfast。具体操作如下打开Options → Target确认Device选的是带FPU的型号。在Target页Floating Point Hardware选择Double Precision FPU如果是H743/H750或Single Precision FPU如果型号只带单精度FPU比如部分低端M4。打开Options → C/C → AC6 Compiler在Miscellaneous Controls里输入一行-ffp-contractfast注意不要用-ffast-math。-ffast-math确实能生成VFMA但它还会关掉NaN/Inf检查、改变除法语义、让编译器把某些不安全的变换也做出来常常导致调试时数值对不上。-ffp-contractfast只影响乘加合并影响面小得多。重新编译然后在Disassembly窗口里搜索vfma验证。这里有个AC6特有的坑如果代码里用了volatile关键字来修饰参与乘加的变量那编译器不会把被volatile修饰的操作数合并进FMA——因为FMA改变了内存访问的次数和顺序。比如volatile float a, b, c; a a b * c; // 即使开了 -ffp-contractfast也不会生成VFMA因为a是volatile这个坑很隐蔽因为volatile的存在让编译器认为每一次读取/写入都必须严格发生所以不能把乘加“融合”成一次操作。如果你的算法在ADC中断里用了volatile修饰的全局变量来传递传感器数据然后又在另一个函数里做浮点运算那这段浮点运算中的乘加很可能不会生成VFMA。解决办法是先拷贝到局部变量处理完再写回。4.2 GCC交叉编译时的常用命令组合对于GCC工具链你一般用的是arm-none-eabi-gcc。一个经过验证的命令行组合是arm-none-eabi-gcc -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -ffp-contractfast -c example.c -o example.o注意几点-mfpu必须和芯片实际FPU匹配。Cortex-M4一般是fpv4-sp-d16Cortex-M7双精度是fpv5-d16Cortex-M33单精度是fpv5-sp-d16。-mfloat-abihard能保证浮点参数走FPU寄存器传递但这和VFMA生成无关不过会影响整体性能。GCC从8.x后默认在-O2以上开启-ffp-contractfast但你最好显式写出来方便代码审查和别人复现。如果你在CMakeLists.txt里维护项目可以在target_compile_options里加target_compile_options(my_target PRIVATE -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -ffp-contractfast )然后编译完用arm-none-eabi-objdump -d -S检查。我在实际项目里遇到的另一个问题是GCC在-O0下绝不生成VFMA-O1有时会。如果你在调试阶段用-O0测试然后发现代码“并没有变快”不是你配置错了是编译器在-O0下根本没做任何合并且保留了所有变量在内存里。这是正常现象别浪费时间调一个-O0版本。4.3 IAR下的对应配置IAR的浮点选项相对独立。你在Options → Compiler → Optimizations里把Level设成High或Speed然后在Extended Options或命令行里加--fp_contractfast这个选项在IAR 8.50.9以上的版本有效。如果找不到可以直接在命令行加。然后Compile再进Disassembly查看。IAR的坑不多但我记得早期版本有一个问题--fp_contractfast只在优化级别High以上才生效如果是Medium则不行。因此如果你做的是IAR下的中等优化调试别指望有VFMA。5. 当VFMA生成之后性能提升有多少数值上会有什么变化5.1 实测数据FIR滤波器和PID控制器的差异我在STM32H743上跑过一个256阶FIR低通滤波器数据是单精度浮点采样率10kHz。编译器用AC6分别编译两个版本一个不开-ffp-contractfast一个开启。开与不开的反汇编差异一目了然不开的版本在循环里是vmul.f32vadd.f32开了变成vfma.f32。实测循环时间从不开的约4.2微秒降到开的约3.1微秒省了约26%。PID控制器方面一个典型的PID代码float pid_step(PID *pid, float setpoint, float measurement) { float error setpoint - measurement; pid-integral error * pid-dt; float derivative (error - pid-prev_error) / pid-dt; float output pid-Kp * error pid-Ki * pid-integral pid-Kd * derivative; pid-prev_error error; return output; }这段代码里的output包含两个乘加。开启-ffp-contractfast之前我得到4条vmul3条vadd开启之后变成3条vfma1条vmul2条vadd。实际单步执行时间从约2.8微秒降到约2.1微秒。对一个10kHz控制环来说省下的0.7微秒在CPU占用率上能体现出来原来CPU占有率约2.8%降到2.1%。看起来不多但如果你在同一个核上还要跑通信协议栈、显示刷新、故障诊断这0.7%的余量可能正好是关键。5.2 数值精度一次舍入是好还是坏VFMA的“一次舍入”在绝大多数应用里是好事因为减少了舍入误差的累积。但要注意它会让计算结果和IEEE严格逐级计算的版本不同。如果你的系统有“和某个参考实现比对误差”的测试用例可能因为VFMA的加入而“误差变大”——虽然从数学上看是更精确了但比对程序认为差得更远。这在一些对绝对一致性有要求的场合需要考虑清楚。比如你在做一个双机冗余系统两套设备跑同一个控制算法一套编译时生成VFMA、一套没有那么这两套系统在长时间运行后累积误差会发散。好在数值级上通常差异极小但如果你的故障检测阈值设得很紧这种现象可能触发误报。另外有一个容易被忽略的关联问题VFMA一次只做一次舍入但FMA指令不会刷新非规格化数denormal。如果你的系统在某些异常输入下产生了极小的浮点数VMLA乘后加在硬件上执行两次舍入可能会把结果刷新为0而VFMA则会保留非规格化数。这两个行为会导致后续分支判断不同。在IEEE严格模式里VMLA的非规格化刷新是“标准行为”在FMA模式下非规格化数是否被刷新由硬件控制位FPSCR.FZ决定。如果你的项目里有“运算结果小于某个极小值时触发保护”的逻辑建议显式处理不要把命运交给编译器。5.3 反汇编之外的性能评估用示波器或引脚翻转优化这种事有时候光看平均周期不够还得看最坏情况。我常用的一个办法是在关键算法开头翻转一个GPIO算法结束后再翻转回来用示波器看高电平宽度。这样测出来的时间包含了中断屏蔽、Cache行为等所有因素。VFMA带来的周期减少在示波器上同样能看到但如果你的代码被其他中断频繁打断高电平宽度的抖动会比较大建议多测几十次取最小值/平均值。我曾经在评估FreeRTOS的调度抖动时踩过一个坑因为任务里用了浮点运算且开了FPU但FreeRTOS的configTASK_FPU_SUPPORT没有配置导致任务切换时FPU寄存器没保存计算结果随机跳变最后曲线惨不忍睹。VFMA本身就依赖FPU寄存器你在项目里用了浮点就必须保证RTOS的FPU上下文切换是开启的。这一步没做好优化性能再高也是白搭。6. 进阶哪些模式下编译器“拼了命”也不会生成FMA6.1 联合体和返回值碰到的坑说到编译器无法生成VFMA的情况有一个很经典如果乘加的结果被写入到联合体union的成员里或者通过指针交叉访问不同数据类型的存储空间编译器可能因为“存储位置可能重叠”alias的风险而拒绝把乘加合并成一条FMA。因为它无法证明内存位置没有重叠所以必须严格按顺序执行多次读、写、运算以保持语义一致。这里我想起了自己之前在电机控制代码里遇到过的情况我用一个联合体同时表示float和uint32_t用来做bit-level操作。代码里写union { float f; uint32_t u; } val; val.f a * b c;这段代码无论我怎么改编译器选项都没生成VFMA。原因就是编译器无法确定a、b、c中是否有某个指针指向了val对象本身所以它得保守地先把乘算完再存一次再加载一次再做加法。解决办法很简单把这个操作改成中间局部变量float tmp a * b c; val.f tmp;编译器瞬间就能生成VFMA。这个坑在C语言里极其隐晦因为union本身不常被视作内存别名的来源。6.2 函数调用和“内存屏障”的影响如果一个乘加表达式中间夹杂了函数调用比如float x a * b; external_function(); float y x c;这也无法合并成VFMA因为编译器必须保证在调用外部函数之前x的计算结果已经落到了可观测的状态。虽然从寄存器内容看x很可能还在某个FPU寄存器里但编译器不敢假设外部函数没有把FPU寄存器全部冲掉。所以在做性能优化时尽量把纯计算的代码块从函数调用中剥离出来让乘加连续出现不要被函数调用打断。这和volatile一样都属于“编译器不敢越雷池”的保守行为。理解了这点你再看那些优化不了的情况就不会觉得编译器“笨”了。6.3 不要指望-O0优化能选中FMA这个前面已经提过但值得再强调一次。-O0的目的就是“快速编译、容易调试”它会把所有变量都分配到内存所有表达式都拆成最小的语句目的就是让你在调试器里能把每条C语句映射到对应指令。不要指望在-O0下看到VFMA。我见过太多人在调试模式下测试性能然后说“编译器优化没用”——这不是编译器的错。7. 遇到性能瓶颈时的排查思路先看算法再抠指令7.1 用Profiler定位热点再决定是否花时间抠VFMAVFMA很香但它不是万能的。我在优化一个传感器融合算法时最开始也以为编译器没生成VFMA是性能瓶颈花了很多时间去配选项、翻反汇编。后来用STM32CubeMonitor的实时跟踪或者ARM的DSTREAM跑了一下Profiler发现真正耗时的其实是一个sqrtf和一个atan2f——这俩是软件函数库实现的根本没有硬件指令。就算我把所有乘加都换成VFMA性能提升也不超过5%。所以一个切身的建议是先确认你的性能瓶颈确实在浮点乘加密集的循环里。可以用ARM官方的CMSIS-DSP库替换自己的手写乘法循环做个对照如果CMSIS-DSP版本比你的快了3倍以上那问题可能不只是少了VFMA更可能是你的循环访存模式很差、或者编译器没有做循环展开甚至可能只是你没开-O3。VFMA毕竟是一条指令一条指令在总执行时间里的占比通常不到30%。7.2 检查Cache、内存对齐和总线带宽很多浮点循环跑得慢跟VFMA没关系而是数据不在Cache里。STM32H7系列有D-Cache和I-Cache如果你的输入数组很大且没有做内存对齐每次访问都可能导致Cache行替换cache line fill这个开销比一条VFMA的周期高出两个数量级。用__ALIGNED(32)或者在链接脚本里把大数据区放到独立的RAM段配合SCB_EnableDCache()往往比折腾编译器选项更有效。我见过一个项目把浮点数组从默认的DTCM RAM挪到AXI SRAM并开启D-Cache之后FFT时间直接降了一半。这不是VFMA的功劳但也是“编译器硬件配置”综合调优的一部分。7.3 编译器乱序调度和双重发射的影响Cortex-M7是双发射的但FPU指令和整数指令占用不同的发射端口。纯浮点乘加序列在M7上并不能完全利用双发射能力因为两条连续的FPU乘加指令之间可能有依赖而FPU流水线的latency是3个周期视具体配置而定如果循环里没有足够的独立指令填满延迟槽VFMA的优势就会减少。这时候可以做的是“解循环依赖”比如FIR滤波器里不要用acc acc coeff[i] * x[n - i]这种累加模式而是分别累加偶数项和奇数项最后再合并float acc0 0, acc1 0; for (i 0; i N; i 2) { acc0 coeff[i] * x[n - i]; acc1 coeff[i1] * x[n - i - 1]; } return acc0 acc1;两路独立的累加链可以让M7的FPU流水线同时在处理两条FMA延迟被互相掩盖吞吐量几乎翻倍。这个手法比单纯追求VFMA影响更大而且和编译器是否生成VFMA无关。8. 一个完整的实操案例把CMSIS-DSP风格FIR改成可向量化的乘加循环8.1 代码结构设计这里我给一个可以直接拿去验证的demo循环。假设你有一组float系数coeff[256]和输入数据x[512]要做一个256阶FIR。最简单的写法float fir_basic(const float *x, const float *coeff, int n_tap) { float acc 0.0f; for (int i 0; i n_tap; i) { acc coeff[i] * x[n_tap - 1 - i]; } return acc; }这段代码在-O2-ffp-contractfast下GCC和armclang都能生成VFMA。但如果你用的是AC5老版本可能要额外注意循环展开的策略。无论如何先验证反汇编。8.2 编译和验证Keil AC6命令行关键部分armclang -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -ffp-contractfast -c fir.c -o fir.oGCCarm-none-eabi-gcc -mcpucortex-m7 -mfpufpv5-d16 -mfloat-abihard -O2 -ffp-contractfast -c fir.c -o fir.o反汇编能看到类似vfma.f32 s0, s1, s2说明生成成功。如果看到vmla.f32也算乘加指令但vmla是“先乘后加且中间结果舍入”的老式指令它比vfma少一次舍入的精度优势但执行周期和vfma基本一样。所以在性能上都行但如果你的项目还要求做数值对比vfma和vmla的结果会有区别。8.3 结合路累加提升流水线利用把上面的demo改一下用两路累加float fir_dual(const float *x, const float *coeff, int n_tap) { float acc0 0.0f, acc1 0.0f; int i 0; for (; i 1 n_tap; i 2) { acc0 coeff[i] * x[n_tap - 1 - i]; acc1 coeff[i1] * x[n_tap - 2 - i]; } for (; i n_tap; i) { acc0 coeff[i] * x[n_tap - 1 - i]; } return acc0 acc1; }编译后在反汇编里能看到两组独立的VFMA交替出现。在Cortex-M7上这一版比单累加版大约再快25%到30%因为两条VFMA之间没有依赖能够更好地利用FPU流水线的双发射能力。9. 常见问题速查表现象可能原因处理方式反汇编里是vmulvadd-ffp-contract未开启或优化级别过低开启-ffp-contractfast确认-O2以上完全没有FPU指令全是__aeabi_fmulFPU选项没开或芯片选型不带FPU检查Target → Floating Point Hardware确认设备型号运行时进入HardFault启动文件未开FPU寄存器访问权限确保启动代码里设置了CPACR寄存器或者用CMSIS自带的SystemInit开了-O3但还是没有VFMAvolatile修饰了操作数先拷贝到局部变量再运算生成的是vmla不是vfma编译器版本或浮点模型差异vmla在性能上等价但精度上不如vfma有VFMA但整体性能提升不明显瓶颈不在乘加指令上用Profiler定位热点检查Cache、内存访问、函数调用开启-ffast-math后出现了奇怪的NaN行为-ffast-math范围太广换用-ffp-contractfast不要全局使用-ffast-math双机冗余系统运行结果有微小差异有的用VFMA有的不用保证两套系统编译选项一致或统一关闭FMA做严格IEEE语义10. 比VFMA更重要的如何系统性地看待“编译器优化”编译器的优化选项再丰富也只是“把代码翻译成更高效指令”的一个工具。真正有价值的是你写代码的方式是否让编译器敢于做这些优化——比如避免volatile滥用、减少函数调用间的状态混合、给编译器足够的别名假设、合理使用局部变量而不是全局变量。我在某个项目里看到有同事在性能关键的循环内直接访问一个结构体里的成员写法是obj-coeff[i]。因为编译器无法证明obj-coeff和输出数组不重叠所以每次循环都要重新加载obj-coeff的地址。改成本地指针const float *c obj-coeff; float *out obj-out;之后编译器不仅能生成VFMA还能做更多的循环展开。这类“结构性优化”有时候比配十个编译选项还有效。另外一个容易被忽略的点是编译器优化等级并不是越高越好。-O3在GCC下会引入自动向量化但对Cortex-M4/M7这类有VFP但NEON支持不完整的核心来说自动向量化可能生成低效代码甚至把原本能用单周期VFMA完成的循环变成一连串栈操作。我在一个FFT工程里就遇到过-O3比-O2还慢10%的情况。原因是GCC尝试把浮点循环向量化成NEON方式M7的NEON是可选项但芯片实际不支持结果只能退化为软件模拟。后来我用-O2 -ffp-contractfast -fno-tree-vectorize性能比-O3好得多。所以我一般推荐的性能优化顺序是先把算法复杂度降下来这是最大的杠杆再用本地指针、局部变量、去除volatile等写法上让代码“对编译器友好”然后选合适的优化等级和FPU选项最后才去反汇编里逐条抠FMA。VFMA是个锦上添花的优化而不是雪中送炭。不过我敢说在浮点乘加密集的控制类和信号处理类代码里VFMA是“性价比”非常高的一条指令。它一次帮你做了两件事还省了寄存器压力对功耗也有轻微好处。至少在我做过的几个项目里遇到编译器不生成VFMA时调整选项之后的反汇编一眼就能看出差别性能提升也实打实。如果你现在手头正好有代码跑得不够快不妨先按上面的步骤检查一遍编译器输出。如果确认已经有了VFMA但性能还是不够那问题多半不在指令上而是整个算法的数据流或者访存模式。祝顺利。
返回列表