如果你的代码直接与硬件交互或代码执行在其他core上,或直接执行加载或写指令,或修改页表,你需要意识到内存序的问题。
如果你是应用开发者,硬件交互可能通过设备驱动,与其他core的交互通过pthread或其他多线程API,与页表内存系统的交互是通过操作系统。所有这些情况,
内存序问题都有相关代码为你解决。但是,如果你在写操作系统内核或设备驱动,或实现hypervisor,JIT编译器或多线程库,你必须对ARM架构的内存序的规则有好的理解。你必须保证当你的代码需要显示的内存访问序时,你能够 通过正确的屏障来完成。
ARMv8架构使用弱时序的内存模型。通用的术语为这意味着内存访问的时序不要求与程序加载和存储操作的顺序一样。处理器能够重排内存读操作。写也可以重排(比如,写联合)。结果,硬件优化,比如cache和写buffer的使用,可以优化处理器性能,这意味着在处理器和外部内存要求的带宽可以被减小且外部内存访问相关的时延被隐藏。
对普通内存的读和写可以通过硬件被重排,仅受限于数据的依赖和显示的内存屏障指令。某些情况要求强时序。你可以通过页表项的内存类型属性为core提供这个信息。
高性能的系统可能支持一些技术如预测内存读,指令的多发射,或乱序执行,与其他技术一起提供硬件的内存访问重排的可能:
指令的多发射
处理器可能在一个cycle发出和执行多条指令,因此在后的指令可能与前面的指令同时执行。
乱序执行
很多处理器支持非依赖指令的乱序执行。当一个指令由于等待之前指令的结果而stall时,处理器可以执行后续的没有依赖的指令。
预测
当处理器遇到条件指令时,比如分支,它可以在它确切知道那个指令被执行前可以预测的开始执行指令。因此,如果条件证明预测是正确时,结果很快就出来了。
预测性的加载
如果一个加载指令读取cacheable位置是预测性的执行,这回导致cache line的填充,并可能回收当前存在的cache line。
加载和存储的优化
由于对外部内存的读和写有很长的时延,处理器通过合并多个存储指令到一个大的事务来减少转换次数。
外部内存系统
在很多复杂的片上系统上,存在多个agent有能力发起转换和多路由给slave设备。一些这样的设备,如DRAM控制器,有能力从不同的master接受同步的请求。事务通过内联被缓冲和重排。这意味着从不同master的访问可以因此使用不同的cycle来完成并可能超过其他。
cache一致性多core系统
在多core处理器中,硬件cache一致性可能在core迁移cache line。因此不同的core可能在不同的顺序看到cache内存位置的更新。
优化的编译器
一个优化的编译器可以重排指令来减少时延或充分利用硬件特征。它通常提前进行内存访问,让内存访问更早,这样在需要该值时有更多的时间完成。
在单core系统中,这种重排通常对编程者是透明的,因为单个处理器可以检查危险并保证数据依赖充分考虑。但是,当多个core共享内存时,内存序的考虑就变得很重要了。本章讨论了与多处理相关的话题,多个执行线程的同步。它也讨论了架构定义的内存类型和规则和他们是如何控制的。
1 内存类型
ARMv8架构定义了两种互斥的内存类型。内存的所有区域可以配置为这两种中的一种,Normal和Device。第三种内存类型,强时序,为ARMv7架构的一部分。这种类型和Device类型差别很少,因此在ARMv8中忽略。
除了内存类型,属性也提供了对可缓存性cacheability,共享性shareability,访问和执行权限的控制。shareable和cache属性仅涉及Normal内存。Device区域通常为non-cacheable和outer-shareable。对于cacheable位置,你可以使用属性来指明处理器的cache分配策略。
内存类型不是直接在转换表项中进行编码的。相反,每个block项指定了3个位来索引内存类型的表。这个表存储在内存属性指示寄存器MAIR_ELn。这意味着表有8个条目且每个条目有8位。如图所示。
虽然转换表的block项自身不直接包含内存类型编码,处理器中的TLB项通常包含某个表项的该信息。因此修改MAIR_ELn只有在ISB指令和TLB无效化指令后才可以被看到。
1.1 Normal内存
你可以对内存中的所有代码和大多数数据区域使用normal内存。Normal内存的例子包括在物理内存中的RAM,FLASH,或ROM。这种类型的内存提供最高的处理器性能因为它是弱时序的且在处理器上限制较少。处理器可以重排,重复,以及合并访问到Normal内存中。
更重要的,标记为Normal的地址位置可以被处理器预测性访问,因此数据或指令从内存中读出而不需要在程序中明确涉及,或在实际引用之前。这种预测性的访问可以由于分支预测,预测性cache line填充,乱序的数据加载或其他硬件的优化而发生。
为了更好的性能,通常将应用代码和数据标记为Normal,当强制的内存序要求时,你可以通过使用明确的屏障操作来完成。Normal内存实现了弱时序内存模型。对于其他normal访问或Device访问,不要求Normal访问来完成。
但是,处理器必须能够处理地址依赖带来的危险。
比如,考虑如下简单的代码时序:
STR X0, [X2]
LDR X1, [X2]
处理器通常保证X1中的值为写入X2的值。
这也应用于很多更复杂的依赖。
考虑如下时序:
ADD X4, X3, #3
ADD X5, X3, #2
STR X0, [X3]
STRB W1, [X4]
STRH W2, [X5]
在这个例子中,访问发生在彼此重叠的地址中。处理器必须保证如果STR和STRB顺序发生时内存被更新,一次LDRH返回最新的值。处理器将STR和STRB合并到一次访问是有效的,被写入正确的数据。
1.2 Device内存
你可以对所有的内存区域使用Device内存,包括访问可能有副作用。比如,对FIFO位置或timer读是不可重复的,因为每次读都返回不同的值。对一个控制寄存器做写可能触发出一个中断。它通常仅用于系统中的外设。Device内存类型对core有更多的限制。
预测性数据访问不能发送到标记为Device的内存区域。仅有一个不通用的例外。如果NEON操作用于从Device内存读取bytes,处理器可能会读取未显示引用的bytes,若它们位于一个对齐的16bytes块,该块包含一个或多个显示引用的bytes。
尝试执行从标识为Device的区域的代码通常不可预测。实现可能处理指令获取像它是正常的non-cacheable属性的内存位置,或它会产生权限fault。
这里有四种不同类型的device内存:
- Device-nGnRnE 最强约束(等于ARMv7架构中强时序内存)
- Device-nGnRE
- Device-nGRE
- Device-GRE 最弱约束
后缀如下三个属性:
Gathering or non Gathering(G or nG)
这个属性决定这个内存区域的多个访问是否合并到一个单独的总线事务。如果地址被标记为nG,在内存总线上的访问的数目和大小必须匹配代码中明确的访问数目和大小。若地址被标记为G,处理器可以 将两个byte写合并成一个半字的写。
对于标识为G的区域,多个对相同内存区域的内存访问可以被合并。比如,若程序读相同的位置两次,core仅需要发起一次读且对两个指令发相同的指令。对于从nG区域读,数据值必须从设备读取。它不能从写buffer或其他地方读。
Re-ordering(R or nR)
这决定对相同的设备是否与其他访问进行重新排序。若地址被标记为nR,然后对相同块的访问一直以程序的顺序执行。块的大小是由实现定义的。当块的大小很大时,它可以跨越几个表项。在这种情况下,对于标识为nR的其他访问,遵守顺序规则。
Early Write Acknowledgement(E or nE)
这决定在处理器和从设备之间的写buffer是否被允许发送一个写完成的应答。若地址被标记为nE,写响应必须来自外设。若地址被标记为E,由buffer负责发出写接受,在终端设备真正接受之前。本质上这是一个发往外部内存系统的信息。