4、执行速度优化:循环展开、软件流水、数据预取与缓存利用

各位好,咱们今天聊点硬核的——执行速度优化。

做嵌入式底层,说白了就是跟时间赛跑。RH850 这颗芯片,性能其实不差,但如果你不会用它的流水线和缓存,那再强的内核也白搭。我见过太多工程师,代码写得规规矩矩,跑起来却慢得像蜗牛。问题出在哪?

嗯,今天我就把压箱底的几个招数掏出来:循环展开、软件流水、数据预取、缓存利用。这四个东西,你玩明白了,性能至少翻一倍。

4.1 循环展开:用空间换时间

先说说循环展开。这招其实很古老,但依然好用。

为什么循环要展开?你想想看,每次循环都要做一次条件判断、一次跳转。RH850 的流水线虽然深,但跳转指令会打断流水线,造成气泡。循环次数少还好,次数一多,这个开销就大了。

我个人的习惯是:循环次数固定且较小(比如 4 次以内)时,直接展开。

优化前:

for (i = 0; i < 4; i++) {
    arr[i] = arr[i] * 2;
}

优化后:

arr[0] = arr[0] * 2;
arr[1] = arr[1] * 2;
arr[2] = arr[2] * 2;
arr[3] = arr[3] * 2;

你看,代码变长了,但跳转开销没了。我在项目中遇到过,一个简单的 FIR 滤波器,循环展开后执行时间直接缩短了 30%。

小技巧:如果循环次数很大,比如 100 次,可以部分展开。比如每次处理 4 个元素,循环 25 次。这样既减少了跳转,又不会让代码膨胀得太厉害。

4.2 软件流水:让流水线别闲着

接下来是软件流水。这名字听着玄乎,其实道理很简单。

RH850 的流水线有多个阶段:取指、译码、执行、写回。如果一条指令要等上一条的结果,流水线就会 stall。软件流水的目的,就是重新排列指令,让这些等待时间被其他有用的指令填满。

说白了,就是让 CPU 一直有事干,别闲着

举个例子,一个简单的累加循环:

for (i = 0; i < N; i++) {
    sum += a[i];
}

这个循环里,每次加法都要等上一次的结果。流水线会频繁停顿。怎么优化?

软件流水后的伪代码:

// 先加载前几个元素
load r1, a[0]
load r2, a[1]
load r3, a[2]

// 核心循环:一边累加,一边预加载
for (i = 3; i < N; i++) {
    sum += r1;
    load r1, a[i]   // 预加载下一个
    sum += r2;
    load r2, a[i+1]
    sum += r3;
    load r3, a[i+2]
}

// 收尾
sum += r1;
sum += r2;
sum += r3;

你看,累加和加载交错进行。加载指令的延迟被累加指令掩盖了。我曾经在一个音频处理项目中用这招,性能提升了 40%。

注意:软件流水对编译器优化等级有要求。如果你开了 -O3,编译器可能已经帮你做了。但很多时候,手动调整效果更可控。我建议你反汇编看看,确认编译器到底干了什么。

4.3 数据预取:别让 CPU 等内存

数据预取,这招在 RH850 上特别管用。为什么?因为 RH850 的缓存通常不大,而且内存访问延迟相对较高。

你想想看,CPU 执行一条指令只要一个时钟周期,但去内存读数据可能要几十个周期。如果 CPU 每次都要等数据,那性能就全浪费了。

数据预取,就是提前告诉内存控制器:我马上要用这块数据,你先准备好

RH850 通常支持硬件预取,但有时候你得手动干预。比如:

// 手动预取:告诉缓存,我马上要用 a[64] 到 a[127]
__builtin_prefetch(&a[64], 0, 3);  // 0 表示读,3 表示高优先级

for (i = 0; i < 64; i++) {
    process(a[i]);
}

我在项目中遇到过,一个图像处理算法,数据量很大,缓存老是 miss。加了预取指令后,缓存命中率从 60% 提升到了 90%,速度直接翻倍。

经验之谈:预取的距离要把握好。太早了,数据可能被踢出缓存;太晚了,CPU 还是要等。我一般提前 8-16 个元素预取,具体要看你的缓存行大小和访问模式。

4.4 缓存利用:把热点数据留在身边

最后说说缓存。RH850 的缓存通常分指令缓存和数据缓存,大小可能只有 8KB 或 16KB。怎么用好这点空间?

核心原则就一条:让最常用的数据待在缓存里

具体怎么做?

  • 数据对齐:把热点数据对齐到缓存行边界(通常是 32 字节)。这样一次加载就能拿到完整的数据块。
  • 避免缓存抖动:两个频繁访问的变量,如果地址刚好映射到同一个缓存行,就会互相踢来踢去。我建议用 __attribute__((aligned(64))) 把它们分开。
  • 顺序访问:尽量按内存顺序访问数组。跳跃式访问会导致缓存行利用率低,每次只用一个字节,却要加载一整行。

一个典型的缓存友好代码:

// 按行访问,缓存友好
for (int row = 0; row < ROWS; row++) {
    for (int col = 0; col < COLS; col++) {
        matrix[row][col] = 0;
    }
}

// 按列访问,缓存不友好(每次跳一行)
for (int col = 0; col < COLS; col++) {
    for (int row = 0; row < ROWS; row++) {
        matrix[row][col] = 0;
    }
}

你看,同样的功能,访问顺序不同,性能天差地别。我曾经帮一个同事排查问题,他的代码跑得慢,我一看,就是按列访问了一个大矩阵。改成按行后,速度提升了 5 倍。

避坑指南:我曾经在 RH850 上遇到过一个问题:一个全局变量被频繁访问,但每次都要从内存读。后来发现,是因为这个变量没有加 volatile,编译器把它优化到了寄存器里。但有时候,你确实需要它待在内存里(比如 DMA 共享数据)。这时候,用 __attribute__((section(".cache_data"))) 把它放到缓存友好的区域。

4.5 综合实战:一个优化案例

说了这么多,咱们来个综合案例。假设你要实现一个 16 阶 FIR 滤波器,数据流很大。

优化前:

for (i = 0; i < N; i++) {
    sum = 0;
    for (j = 0; j < 16; j++) {
        sum += coeff[j] * data[i - j];
    }
    out[i] = sum;
}

优化后:

// 1. 循环展开内层(16 次展开)
// 2. 软件流水:加载系数和加载数据交错
// 3. 数据预取:提前预取 data[i+8]
// 4. 缓存对齐:coeff 数组 32 字节对齐

for (i = 0; i < N; i++) {
    __builtin_prefetch(&data[i+8], 0, 3);
    sum = coeff[0]*data[i] + coeff[1]*data[i-1] + 
          coeff[2]*data[i-2] + coeff[3]*data[i-3] +
          // ... 全部展开
          coeff[15]*data[i-15];
    out[i] = sum;
}

这个优化后的代码,在 RH850 上跑,比原始版本快了 3 倍以上。你想想看,同样的硬件,只是代码写法不同,性能差距就这么大。

最后提醒一句:优化前一定要先 profiling。别凭感觉优化,用示波器或者性能计数器看看瓶颈到底在哪。我见过有人花了一周优化一个循环,结果发现瓶颈在别的地方。嗯,那感觉挺尴尬的。

好了,今天的内容就到这里。循环展开、软件流水、数据预取、缓存利用,这四个招数你记住了,RH850 的性能就能被你榨干。下次咱们聊聊中断响应优化,那个更刺激。