有一个比较古老的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)。
汇编代码分析
下面通过一个基于ARM裸机开发工程的简单实例,讲解这两种不同写法被编译器翻译成的最终ARM汇编代码。
测试代码如下:

Makefile如下:
TARGET=gcd
TARGETC=main
all:
arm-linux-gnueabihf-gcc -lto -g -c -o $(TARGETC).o $(TARGETC).c
arm-linux-gnueabihf-gcc -lto -g -c -o $(TARGET).o $(TARGET).s
arm-linux-gnueabihf-gcc -lto -g -S -o $(TARGETC).s $(TARGETC).c
arm-linux-gnueabihf-ld $(TARGETC).o $(TARGET).o -Tmap.lds -o $(TARGET).elf
arm-linux-gnueabihf-objcopy -O binary -S $(TARGET).elf $(TARGET).bin
arm-linux-gnueabihf-objdump -D $(TARGET).elf > $(TARGET).dis
clean:
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占用变大。
看完两种写法的汇编实现,内判与外判的本质区别就很清楚了:写法1牺牲了运行时的分支效率,换来了更紧凑的代码;写法2用代码体积换取执行效率。实际项目中如果你的编译器已经开了-O2,很多情况下编译器会帮你自动完成这个转换;但在嵌入式裸机、编译器优化受限的场景,手动选择写法2仍然是值得考虑的。
更多嵌入式与汇编学习资源,欢迎访问云栈社区。
|