鲲鹏开发重点2––ARM CPU的推测执行(Speculative execution)
鲲鹏开发重点2–ARM CPU的推测执行(Speculative execution)
(老古,大何)
引子:对Memory Fabric的观察
近期比较热门的Gen-Z是一种语义存储(memory-semantic)体系架构,它本身的主要技术优势:
一是能够将DRAM和非易失性存储器及未来的持久性存储技术结合起来;
二是它还使用一种高带宽、低延迟和高效的协议来简化软硬件设计,降低了解决方案的成本和复杂性。
Gen-Z它已经发展到了一定程度,需要更好地定义来适应更大规模的规范与标准,例如在数据中心越来越受欢迎的成熟NVM Express和新兴Compute Express Link(CXL)协议。
前段时间,我们在项目中考虑Memory Fabric某场景时,尝试了内存管理单元MMU的修改,几经周折,我们也成功在Normal空间禁止了cacheable属性。但是,一提到MMU配置我们就谈虎色变,想起曾经的MMU相关死机案例,那都是寻寻觅觅仍然找不到原因的悲伤故事。
于是,我们就跑题了,把Memory Fabric和禁止cacheable属性都放一边,先梳理一下ARM处理器MMU配置相关的问题,也聊一下与MMU配置相关性比较强的CPU特性--推测执行(Speculative execution)。由于MMU和Speculative execution都是生僻词,系统工程师和底层软件工程师可能了解多一些,应用软件工程师就很少接触了,本文也主要谈一下MMU和推测执行的关系和约束,不对技术细节展开描述。
MMU,内存管理单元,顾名思义就是用于管理内存的部件,这是CPU内部模块名,需要操作系统来进行空间配置和管理,所以,有时提到的MMU并不完全指硬件部分,也包括软件部分。其作用就是完成VA虚拟地址到PA物理地址的转换,页面大小管理,同时配置地址空间的访问属性,包括Normal(乱序访问)和Device(定序访问)进行区分,cache写回,cache写透,关闭cache等等,配置地址空间的访问权限,如只读,只写,可读可写,XN(不可执行)等等。图示如下:
(图片来源网络)
推测执行,或者叫投机执行,或者叫预测执行,也理解为乱序执行,只是传统意义的乱序执行更多是指令在执行阶段的并行执行,而推测执行更多的是指令在提取和分发阶段的投机行为。考虑分支预测,如果分支判断的条件都还没有计算出来,那么CPU提取哪个分支的指令呢?从流水线上看,推测执行就是允许CPU的Fetch部件根据历史和其它条件预取择”正确”分支的指令,并译码分发和执行其中“无害”的指令。
(图片来源网络)
举个生活中的例子,成研所的餐线还是有很多美味的,我早餐喜欢吃秦云老太婆摊摊面,服务员远远看到我,就马上为我准备清汤了,根本不用等我走到跟前说出“来碗清汤排骨面”。服务员的行为就可以看成推测执行,根据我的历史记录进行预判,并提前准备。
(图片来源网络)
备注:面条劲道,排骨实在
下面进入正题。
一、简介Introduction
伴随着处理器引入具备分支预测和推测执行功能的复杂流水线,正确地对MMU编程变得尤为重要,否则将出现非本意的内存访问,严重时将导致处理器挂起或者系统异常。本指南规定了安全编程MMU以避免此类问题的最低要求,假定读者已经理解了MMU运行的基本原理,如果不了解,需要先看相关编程手册补充MMU知识。
二、推测执行Speculative execution
Cortex-A 系列处理器推测执行指令可以提升性能,通常基于分支预测的结果,如果预测正确,将收到较好的性能收益,如果预测失败,所有推测执行的结果将抛弃,流水线将被清空。推测执行可能导致程序访问非本意的内存区域,无论是用于数据读、指令预取还是地址转换表的遍历。MMU必须正确设置才能防止推测执行对系统产生意外的副作用。
现代处理器的分支预测准确率已经相当的高了,高达95%以上,所以预测错误引入的流水线惩罚也越来越忽略不计,预测失败对性能的影响微乎其微,总体上看是利大于弊的。但是,因为推测执行导致的程序异常和系统影响却不可不关注,通过MMU管理可以避免,就像CPU的乱序执行,需要程序员关注一样,手段是添加DMB等指令来阻止对程序有影响的乱序执行,CPU的推测执行也需要程序员关注,只是,这个程序员是更偏向于系统软件的,需要考虑MMU编程来管理。
如果说乱序执行和推测执行为什么分开描述,因为手册就是这样描述的,所以就只好这样去理解。但是,推测执行更加隐秘,不是软件流程写错了,而是CPU没有按照你熟知的套路出牌,
都是底层的指令级并行技术,软件层面是看不到的,这些并行技术本意是不影响软件意图的,无奈,系统复杂,一旦指令并行影响了应用软件,应用软件程序员抓破头皮也无济于事。
三、推测执行与指令预取规则Speculative execution and instruction prefetching rules
1、除非指令改变系统状态,否则允许推测执行指令。
2、不与内存交互的指令可以推测执行 。(直到内部推测寄存器资源耗尽)
3、针对Nornal属性的读内存操作可以推测执行。(写操作不可以推测执行,针对Device属性和strongly ordered属性的内存不可以推测执行)
4、指令预取将基于预测的程序流进行,包括从分支预测失败后的恢复,指令预取不涉及XN位被置位的地址。
5、推测读操作和指令预取(如果需要)将启动地址转换表walks动作。(发生在地址跨页的时候)
推测执行的影响细化了,并不是我们理解的推测执行什么事情都可以干,对内存无损坏的事情(读访问)可以提前干,对硬件寄存器的读访问不能顺便干。
四、MMU编程规则MMU programming rules
1、所有的转换表条目都必须被编程,不管是使用的Level 1,2和3,以及未使用的虚拟地址转换条目也必须用能报错的编码进行编程,访问敏感的区域必须编程为Device属性和Strongly Ordered Memory属性。
2、包含访问敏感属性的内存区域必须设置XN位。
3、TTBR中设置的cache属性必须与转换表地址范围中编程的cache属性相匹配。
这三点规则细化了MMU编程的注意事项,内存属性要设置正确,cache属性要设置正确,必要时要设置XN位(execute never)。
五、小结Summary
1、Cortex-A 系列处理器的推测执行可能导致不期望的内存访问
2、如果地址转换表没有全编程将导致不期望的内存操作,改变系统状态或者发生处理器挂起。
3、转换表必须完全编程来阻止不期望的副作用。
感觉这三点总结就是一句话,通过MMU的转换表正确的无遗漏的编程阻止处理器推测执行引入的负面影响。
附录:5个例子和FAQs
1、案例回顾1~3
案例1:记得多年前,无线产品线有个特性调试了很久,特性目的就是把一段指令代码搬移到一个临时内存空间,然后跳转过去执行,类似补丁功能,结果总是不对,跳过去就挂。我们怀疑是MMU的空间属性配置错误了,结果一语中的,改一下就正常了。之前是PowerPC处理器,如果是ARM上的原理,应该就是XN属性( Execute-never )关闭就好了。
类似任务的栈空间,预先被配置了不可执行属性,如果要在栈上执行指令,这部分空间就需要关闭XN属性。这个和推测执行无关,只是一个XN属性配置错误的案例。
案例2:还是多年前的一个案例,PowerPC上的,记得是进行单板上FLASH的装备测试,测试过程中经常出现异常,偶尔在FLASH要测试完毕了才挂,原来是FLASH空间后面的地址并不连续,有一个空洞,后面地址空间覆盖一个FPGA硬件,硬件配置了TA(Terminal ACK)的,就是需要外部回ACK确认信号,而MMU配置时,空间配大了,在测试flash的指令流区间,CPU进行了推测执行,读了越界的地址,由于访问的不是真正的硬件地址,Slave硬件无法回ACK,就导致了CPU挂死。后来修改了MMU配置就好了,这个案例和本文的推测执行主题比较符合。
案例3:也是MMU配置问题,也是CPU推测执行导致的,这个案例在接入网和无线都出现了,无线产品线定位代价比较大,攻关组耗时1个多月,接入网搜到无线的经验案例,也耗时三天。问题是因为软件配置的MMU逻辑空间大于实际的物理地址空间,导致CPU预取到错误地址时总线挂起,然后看门狗因为长时间没有喂狗而复位单板。
如果MMU没有正确编程,推测执行就有低概率造成严重后果,正向定位的困难很大,因为PC指针都还没有移动,指令就在执行了,trace信息也肯定不准确的。多审视MMU的编程,逐条目比对变得尤为重要。
2、CPU推测执行示例1~2
以下例子旨在说明推测执行的一些潜在行为,不同处理器的行为会因具体流水线实现而异,同一处理器的行为也会因目标内存系统的速度而异。
推测执行示例1 --分支预测
start MOV r0, #0x002F000
MOV r1, #0xFFFFFFFF
MOV r2, #0
loop LDR r3, [r0], #0x10
CMP r1, r3
ADDNE r2, r2, r3
BNE loop
B next_step
示例代码将地址0x2F000开始的内存中数据累加,当值为0xFFFFFFFF时,退出循环,BNE将不被执行,否则以0x10步长地址递增并循环执行。
BNE指令将被预测为“执行”,导致循环加速,当处理器等待一个LDR完成时,它将推测再次执行循环,进入下一次迭代,导致执行更多的LDR操作,可能直到所有LSU单元都已耗尽为止。这里的预取好像是cache预取一样,CPU提前把数据准备好。
因此,在地址0x0002FFF0之后,可能已经下发了3次LDR操作,包括地址0x30000、0x30010、0x30020等等。
这和传统的软件思维不一样,我们软件工程师通常理解的循环是PC指针走到哪里,执行就到哪里。我们不会想到,CPU执行循环居然可以提前跨越几次迭代,把后面几次迭代的数据读操作提前执行了。程序员本来不期望超出地址0x0002FFF0的读操作,由于推测执行,CPU发出了更多的读操作,也访问到一个新的内存页了。
超过0x0002FFF0的读操作的效果取决于MMU地址转换表编程:
1、Normal Memory配置时,读操作会发生,结果会被抛弃。
2、Device 或者Strongly ordered memory配置时,读操作挂起到预测分支被判决的时候,一旦判断分支预测错误,读操作就被放弃。
如果MMU中存在TLB未命中,访问地址0x00030000可能导致地址转换表遍历,如果虚拟地址0x00030000的条目尚未在转换表中编程,则MMU将获得从内存中返回的随机数据。
推测执行示例2 --指令预取
start MOV r0, #0x002F000
MOV r1, #0xFFFFFFFF
MOV r2, #0
loop LDR r3, [r0], #0x10
CMP r1, r3
ADDNE r2, r2, r3
BNE loop
B next_step
———section boundary——————————
uart_ctrl ; UART control register
uart_read ;UART read register
uart_write ; UART write register
当循环终止时,将出现许多错误预测的分支,多余的指令会自动从流水线上清除,并丢所有的结果。处理器将进入后续的指令执行,开始从"B next_step"的地址预取指令,该指令是Normal内存的顶部地址,下一个区就是Device设备内存区。“B next_step“指令之后没有指令,只会访问敏感的从设备。预取单元将读取一系列"B next_step"地址之后的指令,虽然这个区存放的并不是指令,如果这区设置了“XN“(execute never)属性,预取就不会发生。
指令预取将针对访问敏感的从节点执行一系列读操作,这将改变slave的状态,slave可能不支持这样的交易类型,可能收到“SLVERR”响应,或可能以非法操作响应。如果设计正确,salve应返回“SLVERR”以响应指令提取交易,一旦真正执行分支时,错误响应将被丢弃。
跨区跨页边界的内存访问总是容易出问题,没有红绿灯的十字路口也是交通事故频发,所以,MMU属性的正确设置,就像保证红绿灯完好无损一样重要。
3、FAQs
为什么所有地址转换表项都必须编程?
处理器将始终将从内存返回的数据解释为有效的转换表条目。在没有清晰编程的条目控制下,MMU将处理数据访问导致随机访问内存。任何未使用的内存区域都必须配置。
为什么我不能只编程我使用的内存位置?
推测执行和指令预取可能导致程序员未曾预料到的内存区被访问。程序中的错误也有可能导致访问非预期的内存区域。所有条目都必须编程以捕获这两个事件。
为什么需要将设备内存的XN位设置为1?
阻止处理器从访问敏感的设备空间预取指令,以造成不可预期的结果。
所有的读操作都会推测执行吗?
只有Normal memory区域的内存才支持推测方式执行读操作。
禁用程序流预测(分支预测)是否会防止推测执行吗?
推测执行总是使能的,不能关闭。
禁用程序流预测(分支预测)是否会阻止指令预取吗?
指令预取总是使能的,不能关闭。
- 点赞
- 收藏
- 关注作者
评论(0)