1. 循环展开优化

任务目标

本案例针对嵌入式环境中一个典型的数组累加函数,通过手动循环展开(Loop Unrolling)减少循环控制指令(增量、比较、跳转)的执行次数,从而缩短程序运行时间。同时,对比展开前后编译产物的代码体积,直观理解“以空间换时间”的代价。

环境与依赖

  • 编译器:arm-none-eabi-gcc(或任意支持ARM Cortex-M的交叉编译器),版本无特殊要求。
  • 目标平台:STM32F103(Cortex-M3)或任意ARM裸机环境,无需操作系统。
  • 验证工具:arm-none-eabi-size(查看镜像体积)、arm-none-eabi-objdump(反汇编核对指令数)。

核心实现

以下代码包含两个功能完全等价的累加函数:sum_simple为常规单次迭代版本,sum_unrolled为4次手动展开版本。两者均对长度为64的uint32_t数组求和,输入数据由调用方提供,函数返回累加结果。

#include <stdint.h>

#define ARRAY_LEN 64

/* 常规版本:每次循环处理1个元素 */
uint32_t sum_simple(const uint32_t *data) {
    uint32_t sum = 0;
    for (int i = 0; i < ARRAY_LEN; i++) {
        sum += data[i];
    }
    return sum;
}

/* 4次循环展开版本:每次循环处理4个元素 */
uint32_t sum_unrolled(const uint32_t *data) {
    uint32_t sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
    int i = 0;

    /* 主循环:每次迭代累加4个独立累加器 */
    for (; i < ARRAY_LEN - 3; i += 4) {
        sum0 += data[i];
        sum1 += data[i + 1];
        sum2 += data[i + 2];
        sum3 += data[i + 3];
    }

    /* 处理剩余元素(本例中ARRAY_LEN=64,能被4整除,此循环不会执行) */
    for (; i < ARRAY_LEN; i++) {
        sum0 += data[i];
    }

    return sum0 + sum1 + sum2 + sum3;
}

编译与体积对比

使用-O2优化级别编译(该级别下编译器不会自动完全展开此循环,便于观察手动展开效果),生成目标文件并查看代码段体积:

arm-none-eabi-gcc -c -O2 -mcpu=cortex-m3 -mthumb loop_unroll.c -o loop_unroll.o
arm-none-eabi-size loop_unroll.o

预期输出(text列为代码体积,单位为字节):

   text    data     bss     dec     hex filename
    112       0       0     112      70 loop_unroll.o

将sum_unrolled函数注释掉后重新编译,对比text体积变化。典型结果:展开版本比简单版本大约增加40~60字节的代码空间。

运行与验证

在裸机环境中,通过以下方式验证功能正确性与性能差异:

  1. 功能验证:构造一个包含已知数据的数组(如data[i] = i),分别调用两个函数,断言返回值相等且等于预期累加和(0+1+...+63 = 2016)。
  2. 性能对比:在Cortex-M3上,通过DWT->CYCCNT寄存器(需先使能DWT)或GPIO翻转配合示波器测量两个函数的执行周期数。预期sum_unrolled比sum_simple快约20%~30%(循环控制开销占比越高,提升越明显)。
  3. 反汇编核对:执行arm-none-eabi-objdump -d loop_unroll.o,观察sum_unrolled函数体内循环分支指令(B/BNE)数量明显少于sum_simple,证明循环开销确实减少。

通过判定标准:两个函数返回结果一致;sum_unrolled的代码体积大于sum_simple;反汇编显示展开版本循环跳转指令占比下降。

关键点说明

  • 本例中数组长度64恰好能被展开因子4整除,因此尾部处理循环实际不会执行。若数组长度不固定,需保留尾部循环以保证正确性。
  • 使用4个独立累加器(sum0~sum3)而非单个累加器连续累加4次,是为了打破循环间的数据依赖,允许编译器在Cortex-M3上利用指令级并行(虽然M3无乱序执行,但可减少因等待前一次加法结果而产生的流水线停顿)。
  • 展开因子并非越大越好:寄存器压力增加可能导致编译器将变量溢出到栈上,反而降低性能。对于Cortex-M3,4路展开通常是经验上的较优选择。