luyism 发表于 2024-6-25 21:20

《嵌入式软件的时间分析》Cortex-M3的三级流水线与分支预测【阅读笔记3】

本帖最后由 luyism 于 2024-6-25 21:25 编辑

## 《嵌入式软件的时间分析》Cortex-M3的三级流水线与分支预测【阅读笔记3】

在本书的第二章2.6小节中有说到指令流水线,但是说的并不是很详细。这里,我将结合ARM V7架构的Cortex-M3内核,进一步探讨其三级流水线和分支预测背后的设计原理。

### 一、背景与重要性

在嵌入式系统的世界里,处理器的效率直接关系到设备的性能、功耗乃至最终产品的竞争力。Cortex-M3,作为ARM公司针对微控制器领域推出的一款高性能、低功耗内核,广泛应用于工业控制、消费电子、汽车电子等领域。它的设计精髓之一便是引入了三级流水线结构,这一设计不仅沿袭自更高端的ARM处理器,而且在保持较高代码密度的同时,显著提升了指令执行速度,这对于资源有限的嵌入式系统来说至关重要。

### 二、Cortex-M3处理器概述

Cortex-M3处理器是ARM公司设计的一款32位嵌入式处理器,广泛应用于微控制器(MCU)市场。它具有高效的性能和低功耗的特点,非常适合需要实时处理的应用。理解Cortex-M3的流水线和分支预测技术,对于优化嵌入式系统的性能至关重要。

#### 2.1 程序计数器(PC)基础

程序计数器(R15)是一个特殊的寄存器,指向当前的程序地址。如果修改它的值,就能改变程序的执行流。在编写汇编代码时,可以通过直接操作PC来进行跳转或计算相关地址操作。

在Cortex-M3 权威指南中有下面这段话,具体含义下面再具体解释:



### 三、三级流水线结构

Cortex-M3处理器采用了三级流水线结构,分别为取指令(Fetch),解码(Decode),和执行(Execute)阶段。这种结构在简化设计和降低功耗的同时,仍然能够提供足够的处理能力。



#### 3.1 取指令(Fetch)

   **预取单元**:在这个阶段,处理器根据程序计数器(PC)的值从内存中预取指令。Cortex-M3支持Thumb-2指令集,该指令集的指令长度可变(16位或32位),因此预取单元需要能够灵活处理不同长度的指令。Cortex-M3的指令缓存(I-Cache)可以加速这个过程,使得指令可以快速读取。

   ```assembly
   ; 伪代码表示取指令阶段
   F: instruction = memory
   ```

指令预取机制使得处理器在执行当前指令时,提前取出下一条指令,从而减少等待时间。指令缓存则存储最近使用的指令,提高缓存命中率,进一步提升取指令速度。

**PC更新**:取指后,程序计数器会自动递增到下一条指令的地址,对于跳转或分支指令,则根据分支目标地址更新。

#### 3.2 解码(Decode)
   
**指令解码器**:取出的指令在这一阶段被解码,处理器确定要执行的操作和涉及的寄存器或内存地址。解码阶段还负责处理一些简单的指令,如立即数运算。

   ```assembly
   ; 伪代码表示解码阶段
   D: operation, operands = decode(instruction)
   ```

Cortex-M3处理器的解码单元能够快速解析指令,并生成相应的控制信号,为后续的执行阶段做好准备。


#### 3.3 执行(Execute)
   在这一阶段,处理器执行指令,进行算术运算、逻辑运算、内存访问等操作。执行阶段的结果可能会写回寄存器或内存。

   ```assembly
   ; 伪代码表示执行阶段
   E: result = execute(operation, operands)
   ```

Cortex-M3的执行单元包括算术逻辑单元(ALU)、乘法器、加载/存储单元等,可以高效地完成各种操作。

**结果写回**:执行完毕后,操作结果会被写回到寄存器或内存中,准备供后续指令使用。

#### 3.4 PC状态变化

在具有三级流水线的嵌入式处理器中,当处理器正忙于执行第 `N` 条指令时,流水线的前探特性已使得第 `N+2` 条指令处于取指阶段,以维持指令执行的连续性和效率。关键在于,程序计数器(PC)并不指向正在执行的指令,而是前瞻地指向正在被取指的指令,即第 `N+2` 条。这一设计旨在提前准备,确保处理器的指令供给不间断。

考虑到处理器指令长度的多样性,PC的增值得以精确计算。在硬件设计中,每次取指令操作提取32位数据,但实际上指令长度可以是16位或32位,这取决于处理器的工作模式:

- **ARM状态**:处理器工作在标准32位指令集(ARM状态)时,每条指令占用4字节。由于流水线机制,当执行第 `N` 条指令时,PC实际上已经超前指向了第 `N+2` 条指令的地址。因此,如果读取PC,得到的值将是当前执行指令地址基础上增加8字节(即两个4字节指令长度)的结果。

```assembly
0x1000: MOV R0, PC ; R0 = 0x1008
```

- **Thumb状态**:切换至Thumb指令集后,大多数指令精简为16位,每条指令仅占2字节。尽管硬件每次依然提取32位,但实际上可以包含两条16位Thumb指令。因此,在Thumb模式下,执行第 `N` 条指令时,PC指示的将是第 `N+2` 条指令的地址,但由于Thumb指令每条2字节,PC增加的值仅为4字节,即当前执行指令地址加4字节。

```assembly
0x1000: MOV R0, PC ; R0 = 0x1004
```

也就是说:

- **ARM7中的“读PC指令的地址+8”**:在ARM7中,假设有一条指令`MOV R0, PC`位于地址0x1000,由于ARM7的特性,读取PC时返回的将是0x1008,即当前指令地址加8。但注意,这是在非Thumb模式下的情况。当你读取PC寄存器的值时,由于前面提到的两个阶段的自增,你会得到一个指向当前指令之后两条指令地址的值,这是因为每次自增都是4字节,合计8字节。

- **Cortex-M3(Thumb模式)的“读PC指令的地址+4”**:在Cortex-M3中,同样执行`MOV R0, PC`,但假设此时代码在Thumb模式下,位于地址0x1000,由于Thumb指令每条2字节且同样经历了两次流水线自增,读取PC时R0将会被赋值为0x1004,即当前指令地址加4。在CM3的Thumb模式下,同样经过两次自增,但每次自增2字节,因此读取到的PC值会指向当前指令之后的下一条指令地址,总共增加4字节。

值得注意的是,Thumb-2指令集是Thumb指令集的一个扩展,它巧妙融合了16位和32位指令,使得Thumb状态下也能实现与ARM指令集相近的代码密度和功能完整性。尽管Thumb-2指令长度可变,上述PC值的增量规则(ARM状态+8字节,Thumb状态+4字节)仍然适用,因为PC始终反映的是取指阶段的地址,而Thumb-2的执行效率和代码布局优化通过编译器在生成代码时予以考虑,不影响PC值的计算逻辑。

ARM ®Architecture Reference Manual ARMv7-A and ARMv7-R edition 手册中有下面这样一段话。



**向PC写入地址**:无论是ARM指令集还是Thumb指令集,向PC寄存器写入一个地址都会导致程序跳转到该地址。这种机制用于实现跳转指令和函数调用等操作。

**Thumb指令集对PC的访问限制**:大多数Thumb指令无法直接访问PC寄存器。这是因为Thumb指令集设计时考虑到了指令长度的限制和简化指令集的目的。然而,某些特定的Thumb指令仍然可以使用PC,例如分支指令。

**ARM指令集对PC的通用访问**:ARM指令集允许对PC寄存器进行更通用的访问。许多ARM指令可以将PC作为通用寄存器使用,例如进行算术运算、加载/存储等。然而,ARM体系结构规范不推荐将PC用于除程序计数器以外的用途。

综上所述,不论是ARM7还是Cortex-M3,程序计数器读取的值受到其内部指令流水线机制的直接影响。在ARM7中,由于使用32位ARM指令,读PC指令时返回的地址会是当前指令地址加8;而在Cortex-M3的Thumb模式下,由于指令长度变为16位,读取PC指令时返回的地址则是当前指令地址加4。

#### 3.5 有无流水线的对比

**无流水线处理器**(也称作单周期处理器)执行指令的过程简单直接:从内存中读取一条指令,解码,执行,然后重复此过程。这种设计直观易懂,但每条指令的执行都必须等待前一条指令完全结束后才能开始,导致处理器在大部分时间里处于等待状态,效率低下。

**引入流水线后的变化**:Cortex-M3的三级流水线设计打破了这一限制。在任何给定时刻,处理器都在同时执行三个不同阶段的操作。形象地说,就像工厂生产线上的装配工,一个负责取零件,一个负责组装,第三个负责检验打包,每个人都在专注自己的任务,无需等待前一个人完成所有工作再开始。这样一来,即便每一步操作的时间没有减少,但由于三个步骤并行进行,整体上大大提高了处理器的吞吐量。

### 四、分支预测技术

为了提高处理效率,Cortex-M3处理器还采用了分支预测技术。分支预测的目的是在遇到分支指令(如条件跳转)时,提前猜测程序的执行路径,从而减少流水线停顿。

#### 4.1 分支预测的工作原理

1. **分支目标缓冲器(BTB)**
   Cortex-M3使用BTB来记录分支指令的历史信息,包括目标地址和执行结果(跳转或不跳转)。当处理器遇到分支指令时,会查询BTB,根据历史信息进行预测。

   ```assembly
   ; 伪代码表示BTB查询和预测
   if BTB.contains(PC) then
       prediction = BTB.prediction
       target_address = BTB.target_address
   else
       prediction = not taken
   ```

2. **更新预测信息**
   当分支指令实际执行后,处理器会根据结果更新BTB中的信息,以提高下一次预测的准确性。

   ```assembly
   ; 伪代码表示BTB更新
   BTB.prediction = actual_outcome
   BTB.target_address = actual_target_address
   ```

分支预测技术的引入,可以显著减少由于分支跳转导致的流水线停顿,从而提高处理器的整体性能。

### 五、优化技巧

理解Cortex-M3的流水线和分支预测技术后,我们可以采用一些优化技巧来提升系统性能。

1. **减少分支指令**
   尽量减少条件跳转和循环分支的使用,因为这些指令会导致流水线停顿和分支预测失败。可以通过无条件跳转、查表等方式来优化代码。

   ```c
   // 优化前
   if (condition) {
       do_something();
   } else {
       do_something_else();
   }

   // 优化后
   do_something_table();
   ```

2. **循环展开**
   循环展开(Loop Unrolling)是另一种优化技术,通过减少循环控制指令的次数,提高指令并行度和缓存命中率。

   ```c
   // 优化前
   for (int i = 0; i < 100; ++i) {
       process(data);
   }

   // 优化后
   for (int i = 0; i < 100; i += 4) {
       process(data);
       process(data);
       process(data);
       process(data);
   }
   ```

3. **指令调度**
   指令调度是重新排列指令的执行顺序,以减少流水线停顿和数据依赖。可以使用编译器的优化选项或手动调整代码。

   ```c
   // 优化前
   int a = b + c;
   int d = e + f;

   // 优化后
   int a = b + c;
   int d = e + f;// 避免数据依赖
   ```

### 六、实战案例:优化图像处理算法

为了更好地说明这些优化技巧,我们以图像处理算法为例。假设我们有一个简单的边缘检测算法,需要对图像进行遍历和处理。

#### 6.1 原始代码


```c
void edge_detection(uint8_t *image, uint8_t *output, int width, int height) {
    for (int y = 1; y < height - 1; ++y) {
      for (int x = 1; x < width - 1; ++x) {
            int gx = -image[(y-1)*width + (x-1)] + image[(y-1)*width + (x+1)]
                     -2*image + 2*image
                     -image[(y+1)*width + (x-1)] + image[(y+1)*width + (x+1)];
            int gy = -image[(y-1)*width + (x-1)] - 2*image[(y-1)*width + x] - image[(y-1)*width + (x+1)]
                     +image[(y+1)*width + (x-1)] + 2*image[(y+1)*width + x] + image[(y+1)*width + (x+1)];
            output = (uint8_t)(sqrt(gx*gx + gy*gy));
      }
    }
}

```


#### 6.2 优化后代码

1. **数据对齐**

确保图像数据对齐,以提高缓存利用率。

```c
uint8_t image __attribute__((aligned(64)));
uint8_t output __attribute__((aligned(64)));

```

2. **循环展开**

减少循环控制指令,提高处理效率。


```c
void edge_detection(uint8_t *image, uint8_t *output, int width, int height) {
    for (int y = 1; y < height - 1; ++y) {
      for (int x = 1; x < width - 1; x += 4) {
            int gx1 = -image[(y-1)*width + (x-1)] + image[(y-1)*width + (x+1)]
                      -2*image + 2*image
                      -image[(y+1)*width + (x-1)] + image[(y+1)*width + (x+1)];
            int gy1 = -image[(y-1)*width + (x-1)] - 2*image[(y-1)*width + x] - image[(y-1)*width + (x+1)]
                      +image[(y+1)*width + (x-1)] + 2*image[(y+1)*width + x] + image[(y+1)*width + (x+1)];
            output = (uint8_t)(sqrt(gx1*gx1 + gy1*gy1));

            int gx2 = -image[(y-1)*width + (x+0)] + image[(y-1)*width + (x+2)]
                      -2*image + 2*image
                      -image[(y+1)*width + (x+0)] + image[(y+1)*width + (x+2)];
            int gy2 = -image[(y-1)*width + (x+0)] - 2*image[(y-1)*width + x] - image[(y-1)*width + (x+2)]
                      +image[(y+1)*width + (x+0)] + 2*image[(y+1)*width + x] + image[(y+1)*width + (x+2)];
            output = (uint8_t)(sqrt(gx2*gx2 + gy2*gy2));

            int gx3 = -image[(y-1)*width + (x+1)] + image[(y-1)*width + (x+3)]
                      -2*image + 2*image
                      -image[(y+1)*width + (x+1)] + image[(y+1)*width + (x+3)];
            int gy3 = -image[(y-1)*width + (x+1)] - 2*image[(y-1)*width + (x+2)] - image[(y-1)*width + (x+3)]
                      +image[(y+1)*width + (x+1)] + 2*image[(y+1)*width + (x+2)] + image[(y+1)*width + (x+3)];
            output = (uint8_t)(sqrt(gx3*gx3 + gy3*gy3));

            int gx4 = -image[(y-1)*width + (x+2)] + image[(y-1)*width + (x+4)]
                      -2*image + 2*image
                      -image[(y+1)*width + (x+2)] + image[(y+1)*width + (x+4)];
            int gy4 = -image[(y-1)*width + (x+2)] - 2*image[(y-1)*width + (x+3)] - image[(y-1)*width + (x+4)]
                      +image[(y+1)*width + (x+2)] + 2*image[(y+1)*width + (x+3)] + image[(y+1)*width + (x+4)];
            output = (uint8_t)(sqrt(gx4*gx4 + gy4*gy4));
      }
    }
}
```

### 七、总结

通过理解Cortex-M3处理器的三级流水线和分支预测技术,并结合实际应用中的优化技巧,我们可以显著提高嵌入式系统的性能。在开发过程中,不仅需要理论知识,还需要不断实践和调整,以找到最适合具体场景的优化方案。

Jacktang 发表于 2024-6-26 07:20

<p>Thumb-2指令集是Thumb指令集的一个扩展,它巧妙融合了16位和32位指令,使得Thumb状态下也能实现与ARM指令集相近的代码密度和功能完整性,这个是这样的</p>

bobde163 发表于 2024-6-26 08:43

<p>有了分支预测之后,自己使用汇编代码写的高精确代码中如果存在判断跳转,会发现这段代码执行时间每次都会不太一样,存在抖动</p>

luyism 发表于 2024-6-26 13:11

Jacktang 发表于 2024-6-26 07:20
Thumb-2指令集是Thumb指令集的一个扩展,它巧妙融合了16位和32位指令,使得Thumb状态下也能实现与ARM指令集 ...

<p>确实如此,Thumb-2是Thumb指令集的一个重要扩展,它结合了16位和32位指令,使得在Cortex-M3这样的处理器上运行时,能够在保持高代码密度的同时实现与ARM指令集相似的功能。Cortex-M3通过Thumb-2实现了高效的指令集架构设计,使其在嵌入式系统中具备更强的灵活性和功能性。</p>

luyism 发表于 2024-6-26 13:12

bobde163 发表于 2024-6-26 08:43
有了分支预测之后,自己使用汇编代码写的高精确代码中如果存在判断跳转,会发现这段代码执行时间每次都会不 ...

<p>你提到的情况确实存在。分支预测在提高执行效率的同时,也引入了不确定性,因为预测失败时需要清空流水线,重新加载正确的指令。这会导致执行时间的不稳定,尤其是在高精度需求的汇编代码中。为了减少这种抖动,可以尝试优化代码结构,减少复杂分支,或者使用一些编译器提供的提示来帮助分支预测器提高准确率。总之需要在效率和确定性之间找到一个平衡点。</p>

peterhzm 发表于 2024-6-26 14:09

<p>ARM M3/4系列使用的都是三级流水吧?最近在看RISC-V系列的芯片,里面是六级流水线设计,但是资料太少了,不知道楼主有没有相关的资料介绍看一下呀?感觉好像RISC-V要找一些资料远远没有ARM系列的资料齐全和好找,ARM的Cortex-M系列有一本权威指南,里面基本涵盖了内核大大小小各方各面的知识,但RV系列的就没有类似这样的书</p>

luyism 发表于 2024-6-26 18:25

peterhzm 发表于 2024-6-26 14:09
ARM M3/4系列使用的都是三级流水吧?最近在看RISC-V系列的芯片,里面是六级流水线设计,但是资料太少了,不 ...

<p>确实,ARM Cortex-M3/M4系列以其成熟的三级流水线设计著称,这也是它们在嵌入式领域广受欢迎的原因之一。RISC-V就像是乐高积木,因为它开放,不同的厂家可以根据自己的想法来搭建。就像你盖房子,可以简单搭个小屋,也可以建个豪华大厦。这就意味着,RISC-V处理器里的流水线设计,真的要看是谁在做这个处理器。有的厂家可能会设计得很简单,就几级流水线,这样处理器耗电少,体积小,特别适合那些电池供电的小设备;而有的厂家则可能为了追求速度和性能,搞很多级的流水线,就像高速公路上加了很多车道,车流(指令)跑得更快,适合做复杂的计算任务。 所以,说到RISC-V的流水线,它不像有些固定的处理器架构那样一成不变,而是很有弹性,每个厂家都能根据实际情况来定制。正因为这样,找资料有时候会觉得不如成熟架构那么方便。但RISC-V的资源库也正在快速成长,比如,RISC-V基金会的官方网站、GitHub上的开源项目、以及各种技术论坛和博客,都是获取RISC-V最新资讯和技术细节的好去处。<br />
我最经也在看一本叫《<a href="https://bbs.eeworld.com.cn/thread-1284713-1-1.html" target="_blank">RISC-V 开放架构设计之道</a>》的书,不过这里面主要是和<a href="https://bbs.eeworld.com.cn/thread-1284713-1-1.html" target="_blank">RISC-V</a>指令集以及ISA架构相关的,我的帖子里就有这本书的下载方式,同时你也可以看看《计算机组成与设计(基于RISC-V架构)》</p>
页: [1]
查看完整版本: 《嵌入式软件的时间分析》Cortex-M3的三级流水线与分支预测【阅读笔记3】