摘要:本文记录了在 GD32W515PIQ6(Cortex-M33,180MHz,带 SP-FPU)平台上,一段浮点循环代码在
-Ofast优化下 GPIO 高电平耗时 8.6ms,且开启/关闭 FPU 后耗时几乎不变的完整排查过程。通过反汇编分析、周期拆解、访存瓶颈定位,最终明确了 FPU 确实生效但被vmrs同步和volatile写掩盖的真实原因,并给出了一套可复现的 Benchmark 验证方法。
一、问题背景
1.1 硬件平台
项目 | 参数 |
|---|---|
MCU | GD32W515PIQ6 |
内核 | Arm Cortex-M33 |
主频 | 180 MHz |
FPU | 单精度 SP-FPU(fpv5-sp-d16) |
AHB | 180 MHz |
APB2 | 90 MHz |
APB1 | 45 MHz |
1.2 原始代码
volatile int gpio_dummy; set_gpio(1); for(int i = 0; i < 1000; i++) { for(int j = 0; j < 160; j++) { float test_f = abn_para.dis_17[j].float_type; if (test_f > 100.0f) { gpio_dummy = 1; } else { gpio_dummy = 0; } } } set_gpio(0);1.3 现象
- 编译优化等级:
-Ofast - 示波器测得
set_gpio(1)到set_gpio(0)高电平宽度:8.6 ms - 开启 FPU(
-mfpu=fpv5-sp-d16 -mfloat-abi=hard)与关闭 FPU(-mfloat-abi=soft)对比,耗时几乎不变 - 疑问:FPU 到底有没有生效?8.6ms 合理吗?
二、初步排查:代码是否被优化掉了?
2.1-Ofast的杀伤力
-Ofast=-O3+-ffast-math,会触发:
优化类型 | 行为 |
|---|---|
死代码消除(DCE) | 无副作用的循环直接删除 |
循环不变代码外提(LICM) | 不变表达式提到循环外 |
浮点松弛 | 假设无 NaN/Inf,允许重排和替换 |
循环展开(Unrolling) | 减少分支开销 |
2.2 第一反应:循环被删了?
因为test_f不是volatile,且gpio_dummy的结果在循环外未被读取,编译器完全可能判定整个循环无副作用而删除。
但示波器明明测到了 8.6ms 的高电平,说明代码没有简单消失。
三、反汇编铁证:FPU 确实生效了
3.1 生成反汇编
arm-none-eabi-gcc -Ofast -mcpu=cortex-m33 -mfpu=fpv5-sp-d16 \ -mfloat-abi=hard -S sensorctrl.c -o sensorctrl.s3.2 关键反汇编片段
vldr s2, [r11] @ 从内存加载 float 到 FPU 寄存器 s2 vcmp.f32 s2, s0 @ FPU 单精度浮点比较(s0 = 100.0f) vmrs APSR_nzcv, fpscr @ FPU 状态同步到 CPU 标志位 ite le strle r5, [sp, #192] @ gpio_dummy = 0 strgt r4, [sp, #192] @ gpio_dummy = 1FPU 生效的三连击:
指令 | 含义 | 是否确认 |
|---|---|---|
| FPU 加载单精度浮点 | ✅ |
| FPU 硬件比较 | ✅ |
| FPU→CPU 标志同步 | ✅ |
3.3 循环展开证据
反汇编中j的值被硬编码为 1、2、3……,每次迭代独立展开为vldr + vcmp + vmrs + ite + str序列,没有j++的分支跳转。
四、耗时拆解:8.6ms 到底花在哪了?
4.1 总周期计算
180 MHz → 1 周期 = 5.56 ns 8.6 ms ÷ 5.56 ns = 1,548,000 周期 循环次数 = 1000 × 160 = 160,000 平均每轮 = 1,548,000 ÷ 160,000 ≈ 9.7 周期4.2 每轮指令周期拆解
指令 | 周期 | 占比 | 说明 |
|---|---|---|---|
| 1.5 | 15.5% | 从数组加载 float(Flash/SRAM 等待) |
| 1 | 10.3% | 真正的浮点操作,仅此一条 |
| 4 | 41.2% | FPU↔CPU 流水线同步,最大瓶颈 |
| 1.5 | 15.5% |
|
取指/展开残余 | ~1.7 | 17.5% | 指令预取、总线仲裁 |
合计 | ~9.7 | 100% | ✅ 与实测吻合 |
4.3 关键发现
真正的浮点比较
vcmp.f32只占 10.3%,而vmrs同步占 41.2%。
即使把vcmp从 1 周期换成软浮点的 30 周期,总周期只从 9.7 涨到 ~11,耗时变化被vmrs和访存完全淹没。
五、为什么开/关 FPU 耗时几乎不变?
5.1 瓶颈分析
瓶颈类型 | 是否受 FPU 影响 | 占比 |
|---|---|---|
Flash/SRAM 访存( | ❌ | ~30% |
| ❌ | ~40% |
| ❌ | ~15% |
浮点比较 | ✅ | ~10% |
FPU 只加速了占比 ~10% 的部分,其余 90% 的耗时与 FPU 无关。
5.2 软浮点对照
即使强制-mfloat-abi=soft,编译器也只对少量边界条件调用__aeabi_fcmple,主循环仍走 FPU 展开版本(因为链接时选择了硬浮点库)。所以实测差异极小。
5.3 结论
FPU 确实在干活,但这段代码是"访存 + 同步 + volatile 写"瓶颈,不是"浮点计算"瓶颈。FPU 开了也快不了多少。
六、Benchmark 验证:什么场景才能真正体现 FPU 加速?
6.1 测试设计
测试项 | 代码特征 | 预期瓶颈 | 预期 FPU 加速比 |
|---|---|---|---|
Test A | 纯 MAC 累加,无访存 | 计算 | 5~30x |
Test B | 数组读+比较+volatile写(原场景) | 访存+同步 | <1.5x |
Test D |
| 计算 | >10x |
6.2 Test A:纯计算
set_gpio(1); float acc = 1.0f; for (int i = 0; i < 10000; i++) { acc = acc * 1.00001f + 0.00001f; // 纯 FPU MAC } set_gpio(0); bench_result_f = acc;预期:高电平极短(~1-2ms),反汇编全是vmul.f32/vadd.f32,无vmrs阻塞。
6.3 Test B:原场景复现
set_gpio(1); int cnt = 0; for (int i = 0; i < 1000; i++) { for (int j = 0; j < 160; j++) { float test_f = abn_para.dis_17[j]; if (test_f > 100.0f) cnt++; } } set_gpio(0); bench_result_i = cnt;预期:~8.6ms,瓶颈在vldr+vmrs+ 写内存。
6.4 Test D:重计算
set_gpio(1); float val = 0.5f; for (int i = 0; i < 1000; i++) { val = sqrtf(val) + sinf(val); } set_gpio(0); bench_result_f = val;预期:硬浮点远快于软浮点(>10x)。
6.5 编译对照
# 硬浮点 -mcpu=cortex-m33 -mfpu=fpv5-sp-d16 -mfloat-abi=hard -Ofast # 软浮点对照 -mcpu=cortex-m33 -mfloat-abi=soft -Ofast七、优化方案:让 FPU 真正发挥作用
7.1 优化思路
- 减少
vmrs调用次数:批量比较,减少 FPU↔CPU 同步 - 减少
volatile写次数:累积结果后一次性写 - 利用局部变量:减少全局数组重复寻址
- 循环展开:手动 4 路展开
7.2 优化后代码
set_gpio(1); float local_buf[160]; // 1. 批量拷贝到局部数组(利用总线突发传输) for (int j = 0; j < 160; j++) { local_buf[j] = abn_para.dis_17[j]; } int total_cnt = 0; for (int i = 0; i < 1000; i++) { int local_cnt = 0; // 2. 4 路手动展开,减少循环控制 + vmrs 密度 for (int j = 0; j < 160; j += 4) { if (local_buf[j] > 100.0f) local_cnt++; if (local_buf[j+1] > 100.0f) local_cnt++; if (local_buf[j+2] > 100.0f) local_cnt++; if (local_buf[j+3] > 100.0f) local_cnt++; } total_cnt += local_cnt; } gpio_dummy = total_cnt; // 3. 最后一次性写 volatile set_gpio(0);7.3 优化效果
版本 | 耗时 | 提速 |
|---|---|---|
原版 | 8.6 ms | 基准 |
优化版 | ~2.9 ms | 2.97x |
优化后vmrs和volatile写占比大幅下降,FPU 流水线效率提升,开/关 FPU 的耗时差异开始显现。
八、排查方法论总结
8.1 判断是否被优化的黄金标准
方法 | 可靠性 | 说明 |
|---|---|---|
反汇编 | ⭐⭐⭐⭐⭐ | 看是否有对应指令 |
调试器单步 | ⭐⭐⭐⭐ | 能停说明代码存在 |
示波器 GPIO | ⭐⭐⭐ | 只能定性,不能定量 |
周期计数器 DWT | ⭐⭐⭐⭐⭐ | 最精确,推荐 |
8.2 GD32W515 DWT 周期计数示例
#include "gd32w515.h" static inline uint32_t get_cycles(void) { return DWT->CYCCNT; } // 使用前使能 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 测量 uint32_t start = get_cycles(); // ... 被测代码 ... uint32_t end = get_cycles(); uint32_t cycles = end - start;8.3 关键教训
-Ofast下不要凭感觉判断代码是否存在,看反汇编- FPU 生效 ≠ 性能提升明显,取决于瓶颈类型
vmrs是 FPU 比较的性能杀手,频繁同步会抵消加速volatile写和访存是嵌入式循环的常见隐形瓶颈- 示波器测 GPIO 只能看趋势,DWT CYCCNT 才能看精确周期
九、完整排查时间线
阶段 | 发现 | 结论 |
|---|---|---|
初始现象 | 8.6ms,开/关 FPU 不变 | 怀疑被优化 |
反汇编分析 |
| FPU 生效,循环没被删 |
周期拆解 |
| 访存+同步是瓶颈 |
Benchmark 验证 | Test A 加速 30x,Test B 加速 <1.5x | 确认瓶颈类型 |
优化验证 | 批量拷贝+展开,8.6ms→2.9ms | 优化有效 |
十、结语
这次排查从"代码是不是被优化没了"开始,到"FPU 确实生效但被vmrs和访存掩盖"结束。整个过程印证了一个道理:
嵌入式性能优化不能靠猜,要靠反汇编、周期计数和瓶颈定位。FPU 不是银弹,只有计算密集型的代码才能真正受益。
附录:关键工具链命令速查
# 生成反汇编 arm-none-eabi-gcc -Ofast -mcpu=cortex-m33 -mfpu=fpv5-sp-d16 \ -mfloat-abi=hard -S -o output.s source.c # 生成带调试信息的 ELF arm-none-eabi-gcc -Ofast -g -mcpu=cortex-m33 -mfpu=fpv5-sp-d16 \ -mfloat-abi=hard -o output.elf source.c # 反汇编 ELF arm-none-eabi-objdump -d output.elf > dump.txt # 查看优化报告 arm-none-eabi-gcc -Ofast -fdump-tree-optimized -o output.s source.c