测试环境参考下面文章:
《手把手教你分析C语言if架构代码最终如何用arm汇编实现》
1、一个古老的C语言笔试题
有一个比较古老的C语言面试题,相信很多老铁都刷到过。
这两种写法的优缺点汇总如下:
写法1
优点:
- 代码结构简洁,整体逻辑集中,一眼看懂 “循环N次,每次走分支”。改造成本低:后续给单次循环增加逻辑、调整分支条件,改动范围小。 缺点:额外开销:循环 n次,每次都执行if判断,循环次数越多,冗余开销越大。CPU 分支预测风险:频繁分支可能造成流水线停顿,嵌入式 / 高频场景下性能损耗更明显。编译器难以做循环展开、常量传播等深度优化。
写法2
优点
- 性能更优:if 仅执行1 次,循环体内纯函数调用,无多余判断,指令更少。利于 CPU 流水线:循环内无分支,分支预测百分百命中,执行效率高。编译器优化友好:单循环体结构简单,更容易被循环展开、寄存器优化。
缺点
- 代码冗余:两个for循环结构完全重复,代码有重复片段。可读性略降:分支嵌套循环,多层结构不如第一种直观。维护麻烦:后续要修改循环体逻辑,两处for都要同步修改,易漏改。
总结 & 选型建议
- a/b 在循环中固定不变(当前场景) 追求性能 → 选写法 2;追求代码简洁、易维护 → 选写法 1。
补充:现代编译器(GCC/Clang)开启-O2及以上优化时,会自动把写法 1 优化成写法 2,二者运行效率基本一致。 a/b
- 在循环内会被修改 只能用写法 1,写法 2 逻辑不成立。代码规范小提示
- 循环次数极大、嵌入式裸机、高频调度场景:优先外判(写法 2)。业务逻辑简单、追求易读易维护、循环次数少:优先内判(写法 1)。
2、 汇编代码分析
下面我们通过一个基于arm裸机开发工程的简单实例给大家讲解,这两个不同写法被编译器翻译成最终的arm汇编代码。 测试代码如下:
Makefile如下:
1 TARGET=gcd
2 TARGETC=main
3 all:
4 arm-linux-gnueabihf-gcc -lto -g -c -o $(TARGETC).o $(TARGETC).c
5 arm-linux-gnueabihf-gcc -lto -g -c -o $(TARGET).o $(TARGET).s
6 arm-linux-gnueabihf-gcc -lto -g -S -o $(TARGETC).s $(TARGETC).c
7 arm-linux-gnueabihf-ld $(TARGETC).o $(TARGET).o -Tmap.lds -o $(TARGET).elf
8 arm-linux-gnueabihf-objcopy -O binary -S $(TARGET).elf $(TARGET).bin
9 arm-linux-gnueabihf-objdump -D $(TARGET).elf > $(TARGET).dis
10 clean:
11 rm -rf *.o *.elf *.dis *.bin
lto (Link Time Optimization,链接时优化)能做的优化(对你代码也生效)
- 跨文件常量传播、死代码删除;跨函数内联、全局循环优化;进一步合并冗余分支、精简指令。
写法1的主函数汇编代码如下:
关键特征
-
- 两层分支嵌套在循环体内 每一轮循环:
cmp(56行) + bge(57) + ble(70行)
- 3 条分支相关指令 必执行一遍。循环总次数:20 次 → 20 次完整条件判断 + 20 次条件分支跳转。指令流被频繁打断代码段体积更小,主函数一共34条(写法2 :45条),只有一套循环框架,没有重复的 for 循环汇编模板。
写法2的主函数汇编代码如下:
关键特征
- 分支开销降到最低,全局仅 1 次比较 + 1 次条件分支,进入循环后再无分支判断。 ARM 流水线持续满载,几乎无气泡。两个循环体内部是纯顺序指令流,只有循环收尾的无条件 / 条件跳转。缺点也很明显, 代码冗余,ROM/Flash 占用变大。
4、代码获取
完整代码获取,【一口Linux 】后台回复:汇编
更多嵌入式知识,请加彭老师好友:yikoupeng
阅读全文
221