嵌入式Linux——解密 ARM 性能优化:LDR 未命中时,为何 STR 还能“插队”?
问题理解
- ARM v6/v7 处理器会对以下指令顺序进行优化
LDR r0, [r1] ; STR r2, [r3] ; - 假如说第一条LDR指令导致缓存未命中,这样缓存就会填充行,并需要较多的时钟周期才能完成。老的ARM处理器会等待这个动作完成,再执行下一条STR指令。而ARM v6/v7 及以后处理器会识别出下一条指令(STR)且不需要等待第一条指令完成(并不依赖r0的值),即先执行STR指令,而不是等待LDR指令完成。
什么叫缓存未命中
- 错误理解:为什么还能LDR指令缓存未命中,那没命中岂不是没能从内存拿到数据?
- “缓存未命中” 不等于 “获取数据失败”
- “缓存未命中” 等于 “在最快的地方(缓存)没找到,必须去下一级更慢的地方(内存)找”
“缓存未命中”时,CPU 的完整处理流程
- 这个流程是自动由硬件(CPU 和内存控制器)完成的,你的程序(LDR指令)不需要操心如何去做,只需要等待它完成。
- 下述流程先抛开多级缓存和指令顺序优化。
-
尝试在“办公桌”(L1 Cache)上寻找
- CPU 拿到地址 [r1]。
- 它首先在最快的存储——你的**办公桌(L1 缓存)**上,寻找这个地址的数据。
- 结果:没找到!这就是**“缓存未命中”(Cache Miss)**。
-
“那怎么办?” —— CPU 停顿 (Stall)
- 你的反应(CPU 停顿):你(CPU)不能继续执行下一条指令
STR r2, [r3],因为你必须先完成 LDR。 - 你“哎呀”一声,停下手中的工作,开始等待。
- 这就是“需要较多的时钟周期”的开始。 CPU 在原地空转,等待数据送达。
- 你的反应(CPU 停顿):你(CPU)不能继续执行下一条指令
-
去“大书柜”(Main Memory / 内存)拿数据
- 因为办公桌(Cache)上没有,你(的内存控制器)只好站起来,走到**大书柜(主内存 RAM)**那里。
- 根据地址 [r1],你在书柜中找到了那份文件(数据)。
- 数据肯定是在“书柜(内存)”里的。(如果连内存里都没有,那就是“缺页异常/Page Fault”,那是另一个更慢的故事了,但 LDR 指令本身假定数据在内存中)。
-
“缓存就会填充行”(Cache Line Fill)
- 这是最关键的一步!
- 你(CPU)非常聪明,你不会只把 r0 需要的那 4 个字节(假设是32位ARM)从书柜拿到办公桌。
- 你猜测:“既然我拿了这份文件,我待会儿很可能也会看它旁边的文件。”(这就是空间局部性原理)
- 于是,你把包含 [r1] 数据的一整“行”(比如一整个文件夹,技术上称为一个 Cache Line,例如 64 字节)全部从“书柜(内存)”搬运到了“办公桌(缓存)”上。
- 这个动作就叫做**“缓存填充行”(Cache Line Fill)**。
-
完成 LDR 指令
- 现在,数据已经成功地被你从“书柜(内存)”复制到了“办公桌(缓存)”上。
- CPU 再从“办公桌(缓存)”上,把 r0 真正需要的那 4 个字节的数据,抓取到“0号抽屉(r0 寄存器)”中。
- LDR r0, [r1] 指令终于完成。
-
CPU 恢复执行
- “等待”结束。
- CPU 继续执行下一条指令
STR r2, [r3]。
什么叫指令顺序优化
-
简单来说,就是 CPU 不严格按照你写的代码顺序来执行指令,而是在保证程序最终结果正确的前提下,打乱顺序,谁的条件先满足,谁就先执行,以此来“压榨”CPU的性能。
-
想象一下,你(CPU)早上起床做早餐,你的“指令列表”如下:
- 烧水(需要10分钟)
- 烤面包(需要3分钟)
- 从冰箱拿果酱(需要30秒)
-
场景一:老的ARM处理器(按序执行 In-Order Execution)
- 这种处理器是个“死脑筋”,它严格按照指令顺序来:
- 执行 烧水:打开水壶。
- 等待:站在原地干等10分钟,直到水烧开。(这就像LDR缓存未命中,CPU在“停顿/Stall”)。
- 执行 烤面包:水烧开后,把面包放进烤面包机。
- 等待:再等3分钟。
- 执行 拿果酱:面包烤好后,跑去冰箱拿果酱。
- 总耗时:10分钟 + 3分钟 + 30秒 = 13.5 分钟。 效率极低!
- 这种处理器是个“死脑筋”,它严格按照指令顺序来:
-
场景二:ARM v6/v7 处理器(乱序执行 Out-of-Order Execution)
-
这种处理器非常聪明,它会“预读”整个指令列表,并分析它们之间的依赖关系:
-
烤面包 需要依赖 烧水 吗?不需要。
-
拿果酱 需要依赖 烧水 或 烤面包 吗?也不需要。
-
于是,它的执行流程变成:
- 执行 烧水:打开水壶。(这是一个耗时操作,CPU知道它需要等,于是把它丢给“水壶”去处理)。
- 立即执行 烤面包:在水壶烧水的同时,把面包放进烤面包机。(这就是你例子中的 STR 指令)。
- 立即执行 拿果酱:在水壶烧水和面包机烤面包的同时,跑去冰箱拿果酱。
- 等待:现在,所有能“偷跑”的指令都执行了,CPU只需等待最长的那个任务(烧水)完成即可。
-
总耗时:取决于最长的那个任务 = 10 分钟。 效率极高!
-
