本帖最后由 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)可以加速这个过程,使得指令可以快速读取。
F: instruction = memory[PC]
指令预取机制使得处理器在执行当前指令时,提前取出下一条指令,从而减少等待时间。指令缓存则存储最近使用的指令,提高缓存命中率,进一步提升取指令速度。
PC更新:取指后,程序计数器会自动递增到下一条指令的地址,对于跳转或分支指令,则根据分支目标地址更新。
3.2 解码(Decode)
指令解码器:取出的指令在这一阶段被解码,处理器确定要执行的操作和涉及的寄存器或内存地址。解码阶段还负责处理一些简单的指令,如立即数运算。
D: operation, operands = decode(instruction)
Cortex-M3处理器的解码单元能够快速解析指令,并生成相应的控制信号,为后续的执行阶段做好准备。
3.3 执行(Execute)
在这一阶段,处理器执行指令,进行算术运算、逻辑运算、内存访问等操作。执行阶段的结果可能会写回寄存器或内存。
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字节指令长度)的结果。
0x1000: MOV R0, PC ; R0 = 0x1008
- Thumb状态:切换至Thumb指令集后,大多数指令精简为16位,每条指令仅占2字节。尽管硬件每次依然提取32位,但实际上可以包含两条16位Thumb指令。因此,在Thumb模式下,执行第
N
条指令时,PC指示的将是第 N+2
条指令的地址,但由于Thumb指令每条2字节,PC增加的值仅为4字节,即当前执行指令地址加4字节。
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 分支预测的工作原理
分支目标缓冲器(BTB)
Cortex-M3使用BTB来记录分支指令的历史信息,包括目标地址和执行结果(跳转或不跳转)。当处理器遇到分支指令时,会查询BTB,根据历史信息进行预测。
if BTB.contains(PC) then
prediction = BTB[PC].prediction
target_address = BTB[PC].target_address
else
prediction = not taken
更新预测信息
当分支指令实际执行后,处理器会根据结果更新BTB中的信息,以提高下一次预测的准确性。
BTB[PC].prediction = actual_outcome
BTB[PC].target_address = actual_target_address
分支预测技术的引入,可以显著减少由于分支跳转导致的流水线停顿,从而提高处理器的整体性能。
五、优化技巧
理解Cortex-M3的流水线和分支预测技术后,我们可以采用一些优化技巧来提升系统性能。
减少分支指令
尽量减少条件跳转和循环分支的使用,因为这些指令会导致流水线停顿和分支预测失败。可以通过无条件跳转、查表等方式来优化代码。
if (condition) {
do_something();
} else {
do_something_else();
}
do_something_table[condition]();
循环展开
循环展开(Loop Unrolling)是另一种优化技术,通过减少循环控制指令的次数,提高指令并行度和缓存命中率。
for (int i = 0; i < 100; ++i) {
process(data<i>);
}
for (int i = 0; i < 100; i += 4) {
process(data<i>);
process(data[i+1]);
process(data[i+2]);
process(data[i+3]);
}
指令调度
指令调度是重新排列指令的执行顺序,以减少流水线停顿和数据依赖。可以使用编译器的优化选项或手动调整代码。
int a = b + c;
int d = e + f;
int a = b + c;
int d = e + f;
六、实战案例:优化图像处理算法
为了更好地说明这些优化技巧,我们以图像处理算法为例。假设我们有一个简单的边缘检测算法,需要对图像进行遍历和处理。
6.1 原始代码
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[y*width + (x-1)] + 2*image[y*width + (x+1)]
-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[y*width + x] = (uint8_t)(sqrt(gx*gx + gy*gy));
}
}
}
6.2 优化后代码
- 数据对齐
确保图像数据对齐,以提高缓存利用率。
uint8_t image[IMAGE_SIZE] __attribute__((aligned(64)));
uint8_t output[IMAGE_SIZE] __attribute__((aligned(64)));
- 循环展开
减少循环控制指令,提高处理效率。
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[y*width + (x-1)] + 2*image[y*width + (x+1)]
-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[y*width + x] = (uint8_t)(sqrt(gx1*gx1 + gy1*gy1));
int gx2 = -image[(y-1)*width + (x+0)] + image[(y-1)*width + (x+2)]
-2*image[y*width + (x+0)] + 2*image[y*width + (x+2)]
-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[y*width + (x+1)] = (uint8_t)(sqrt(gx2*gx2 + gy2*gy2));
int gx3 = -image[(y-1)*width + (x+1)] + image[(y-1)*width + (x+3)]
-2*image[y*width + (x+1)] + 2*image[y*width + (x+3)]
-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[y*width + (x+2)] = (uint8_t)(sqrt(gx3*gx3 + gy3*gy3));
int gx4 = -image[(y-1)*width + (x+2)] + image[(y-1)*width + (x+4)]
-2*image[y*width + (x+2)] + 2*image[y*width + (x+4)]
-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[y*width + (x+3)] = (uint8_t)(sqrt(gx4*gx4 + gy4*gy4));
}
}
}
七、总结
通过理解Cortex-M3处理器的三级流水线和分支预测技术,并结合实际应用中的优化技巧,我们可以显著提高嵌入式系统的性能。在开发过程中,不仅需要理论知识,还需要不断实践和调整,以找到最适合具体场景的优化方案。