4. 循环倒置

任务目标

在嵌入式循环中,若每次迭代都执行与循环无关的条件判断,分支预测器会因结果不稳定而产生惩罚。本案例通过循环倒置(将条件判断移出循环体)消除冗余分支,以时间换空间效率。目标:给定一组传感器数据,统计其中超过阈值的个数,并计算所有数据的平均值——但阈值判断与平均值计算互不依赖,因此可拆分为两个独立循环,使每个循环内部无分支。

核心实现

#include <stdint.h>

#define N 128

// 原始版本:单循环内包含分支
uint32_t count_over_threshold_v1(const int16_t *data, int16_t threshold) {
    uint32_t cnt = 0;
    for (int i = 0; i < N; i++) {
        if (data[i] > threshold) {   // 每次迭代都判断
            cnt++;
        }
    }
    return cnt;
}

// 循环倒置版本:先统计,再累加,循环内无分支
uint32_t count_over_threshold_v2(const int16_t *data, int16_t threshold) {
    uint32_t cnt = 0;
    // 第一遍:只做比较,无分支(利用比较结果直接加0或1)
    for (int i = 0; i < N; i++) {
        cnt += (data[i] > threshold) ? 1u : 0u;
    }
    return cnt;
}

// 若需同时计算平均值,可进一步拆分:
int32_t sum_all(const int16_t *data) {
    int32_t sum = 0;
    for (int i = 0; i < N; i++) {
        sum += data[i];   // 无分支
    }
    return sum;
}

关键说明

上述v2版本通过算术化分支(将if转换为三元运算符)使编译器可生成无跳转的SETcc指令或条件移动,从而避免分支预测失败。若目标平台支持__builtin_expect或SIMD,可进一步优化,但此处保持最小可复现实现。

运行与验证

// 测试代码(需包含上述函数)
#include <stdio.h>
#include <assert.h>

int main() {
    int16_t data[N];
    for (int i = 0; i < N; i++) {
        data[i] = (int16_t)(i * 3 - 100);  // 确定性输入
    }
    int16_t th = 50;
    uint32_t c1 = count_over_threshold_v1(data, th);
    uint32_t c2 = count_over_threshold_v2(data, th);
    assert(c1 == c2);  // 功能等价验证
    printf("v1=%u, v2=%u, 结果一致\n", c1, c2);
    return 0;
}

编译与运行:

gcc -O2 -o loop_invert loop_invert.c && ./loop_invert

通过条件:程序输出v1=26, v2=26(因i*3-100>50即i>50,共128-51=77个?实际计算:i=51时值为53>50,故从51到127共77个)。预期输出:v1=77, v2=77, 结果一致。若需观察分支优化效果,可反汇编对比v1与v2的循环体指令数。

性能对比方法

在目标嵌入式平台(如Cortex-M4)上,可用DWT->CYCCNT寄存器测量两版本执行周期数。通常v2比v1少约20%~30%周期(取决于数据分布)。但本案例不提供实测数据,仅说明测量方法:初始化DWT后,分别对两函数运行1000次取平均周期差。

原始循环(含分支) 每次迭代: → 比较 → 跳转 分支预测失败时 → 流水线冲刷 → 额外3~5周期 循环倒置(无分支) 每次迭代: → 比较 → 加0/1 无跳转指令 → 流水线不中断 → 周期数稳定 →

注意事项:循环倒置适用于循环次数已知且较大的场景。若循环体本身极短(如仅2~3条指令),分支开销占比高,收益明显;若循环次数少(<5),则可能因代码体积增加而得不偿失。本案例中N=128为典型嵌入式传感器批次大小。