我们经常在手册中看到这样的一段文字:当指令对 R15 的读取没有超过任何对 R15 使用的限制时, 读取的值是指令的地址加上 8 个字节。当使用 STR 或 STM 指令保存 R15 时, 出现了上述规则的一个例外。 这些指令可将指令地址加 8个字节保存 (和其他指令读取 R15 一样) 或将指令自身地址加 12 个字节 (将来还可能出现别的数据)。 偏移量是8 还是12 (或是其他数值) 字节取决于 ARM 的实现 (也就是说, 与芯片有关)。 对于某个具体的芯片, 它是个常量。
看到以上文字描述,我们通通是一头雾水,到底如何理解呢,死记硬背好像也不影响使用。但是这个问题不解决还是不畅快,翻阅了数据手册以及网上各种说法,以ARM9为例,终于理解是怎么回事了。
最早期的CPU以51为例,是没有流水线这个概念的,CPU的运行流程也是取指→译码→执行→取指→...循环往复。在执行阶段的时候,PC指针会指向当前运行指令的下一个地址。因此,PC指针总是指向当前正在执行指令的下一条指令。到了ARM体系,由于流水线的设计理念,虽然PC指针还是指向当前正在执行的下一条指令。但是指向已经发生了变化,以下表ARM9为例,假设CPU工作模式在指令长度定长为4个字节,并且CPU无任何跳转指令:
时钟周期 | 一级流水线 | 二级流水线 | 三级流水线 | 四级流水线 | 五级流水线 |
1 | 取指令(取指) | 译码/Reg读(译码) | 移位/ALU(执行) | 数据存储器访问 | Reg写 |
2 | 译码/Reg读(译码) | 移位/ALU(执行) | 数据存储器访问 | Reg写 | 取指令(取指) |
3 | 移位/ALU(执行) | 数据存储器访问 | Reg写 | 取指令(取指) | 译码/Reg读(译码) |
4 | 数据存储器访问 | Reg写 | 取指令(取指) | 译码/Reg读(译码) | 移位/ALU(执行) |
5 | Reg写 | 取指令(取指) | 译码/Reg读(译码) | 移位/ALU(执行) | 数据存储器访问 |
假设第一个时钟周期三级流水线当前正在执行的指令地址为0x30000000,则二级流水线的译码指令地址为0x30000004,第一级流水线的取指令地址为0x30000008,四级流水线和五级流水线对指令地址无影响。
则第二个时钟周期,二级流水线正在执行的指令地址为0x30000004,一级流水线译码地址为0x30000008,五级流水线取指令地址为0x3000000C,其他两级流水线对指令地址无影响。
第三个时钟周期,一级流水线正在执行的指令地址为0x30000008,五级流水线译码地址为0x300000C,四级流水线取指令地址为0x30000010,其他两级流水线对指令地址无影响。
第四个时钟周期,五级流水线正在执行的指令地址为0x30000010,四级流水线译码地址为0x3000014,三级流水线取指令地址为0x30000018,其他两级流水线对指令地址无影响。
第五个时钟周期,五级流水线正在执行的指令地址为0x30000014,四级流水线译码地址为0x3000018,三级流水线取指令地址为0x3000001C,其他两级流水线对指令地址无影响。
通过以上五个时钟周期五级流水线的运行的描述,可以总结以下规律:
1.CPU运行指令地址,每隔一个时钟周期+4
2.CPU译码指令地址,每隔一个时钟周期+4
3.CPU取指令地址,每隔一个时钟周期+4
4.CPU取指令地址总是当前运行指令地址+8
5.CPU译码地址总是当前运行指令地址+4
从而如下推论显而易见:
1.CPU工作在Thumb模式,指令长度为2个字节的时候,上述所有结论结果除2
2.假设发生一次函数调用,当前执行指令为跳转指令,则下一执行指令为跳转指令,从而cpu流水线跳转指令之前缓存的操作是无效的。所以,cpu遇到跳转指令一定会清空流水线,这个时候cpu的工作效率会降低,因为流水线清空了。再考虑一种极端情况,假设跳转的地址与当前运行指令地址差值,超过了cpu缓存的代码区域大小,很荣幸,cpu还要进行一次内存重装载工作。这个时候内存装载开销远远超过了函数调用开销,假设这个函数调用还是在for循环里,并且循坏次数非常大,我只能说悲剧了。这是在代码优化的时候必须要注意的地方,内联函数的出现有一部分原因是基于此的。基于以上理由,我敢肯定,在某些高级语言,进行代码优化的时候,一定会有把for循环里的函数调用改成内联函数这么一种优化办法。所以编写代码的时候,尽量编写顺序执行代码有利于提高运行效率,毕竟流水线不用清空了,尽量少用分支、选择、跳转。再来计算一下返回地址,由于跳转指令导致流水线清空,所以函数调用的返回地址必然是函数调用指令的下一条地址,arm下应当为当前PC指针-4。