术语存储器次序引用为处理器通过系统总线将读(加载)和写(存储)发布到系统存储器的次序。Intel 64和IA-32支持若干种存储器次序模型,依赖于架构的实现。比如,Intel386处理器强制执行程序次序(通常被引用为强次序),通过这个次序,读和写在所有情况下,以它们在指令流中发生的次序被发布在系统总线上。
为了允许指令执行的性能优化,IA-32架构在奔腾4、Intel致强和P6家族处理器中允许违反强次序模型,称为处理器次序。这些处理器次序的变种(这里称为存储器次序模型)允许提升性能的操作,比如允许在被缓存的写之前先读。这些变种中任意一种的目标是为了在维护存储器一致性的同时提升指令执行的速度,甚至在多处理器系统中。
8.2.1小节和8.2.2小节描述了由Intel486、奔腾、Intel酷睿2 Duo、Intel凌动、Intel酷睿Duo、奔腾4、Intel致强和P6家族处理器实现的存储器次序。8.2.3小节给出了例子来描绘基于IA-32和Intel 64处理器的存储器次序模型的行为。8.2.4小节考虑了对字符串操作存储的特殊对待,而8.2.5小节讨论了存储器次序的行为如何通过特定指令的使用而可以进行修改。
8.2.1 Intel® 奔腾® 以及 Intel486™中的存储器次序
Intel奔腾和Intel486遵循处理器次序的存储器模型;然而,它们在大多数情况下作为强次序的处理器进行操作。读和写在总线上总是以被编程的次序出现,除了以下所展示的处理器次序的情况。当所有被缓存的写都Cache命中,并从而不直接写入与读失败正访问的相同的地址时,在总线上,允许读失败在被缓存的写之前执行。
在I/O操作的情况下,读和写都以被编成的次序出现。
软件的意图在于正确操作以处理器次序方式下的处理器(诸如奔腾4、Intel至强和P6家族处理器),而不应该依赖于像奔腾和Intel486这样的相对强次序的处理器。取而代之的是,软件应该通过使用合适的锁或串行化操作,以确保对那些打算在处理器之间控制并发执行的共享变量的访问被显式要求服从程序次序(见8.2.5小节加强或弱化存储器次序模型)。
8.2.2 P6及最近处理器中的存储器模型
Intel酷睿2 Duo、Intel凌动、Intel酷睿Duo、奔腾4和P6家族处理器也使用一个处理器次序的存储器次序模型,这个模型可以进一步被定义为“以存储缓存转运(store-buffer forwarding)方式的写次序”。这个模型可以用以下方式刻画:
在一个针对被定义为可被写回cache的存储器区域的单处理器系统中,存储器次序模型遵守以下原则(注:对单处理器和多处理器系统的存储器次序原则是从执行在处理器上的软件的角度制定的,这里,术语“处理器”引用为一个逻辑处理器。比如,一个支持多个核心以及/或超线程技术的物理处理器被对待为一个多处理器系统。):(译者注:以下所谓的“重新编排”是指原来按照程序次序执行读写操作在某些情况下可以被重新排列为乱序执行,以提升程序执行的性能)
1、读不与其它读作重新编排
2、写不与之前的读作重新编排
3、对存储器的写不与其它写作重新编排,除了以下几个例外——
——与CLFLUSH指令一起执行的写(译者注:CLFLUSH指令的存储器次序相当弱,即使与串行化指令一起执行都可以被乱序掉,呵呵)
——用非暂时的搬移指令(MOVNTI、MOVNTQ、MOVNTDQ、MOVNTPS以及MOVNTPD)所执行的流存储(写);以及
——字符串操作(见8.2.4.1小节)
4、读可以与先前对不同位置的写作重新编排,但不能与先前对相同位置的写作重新编排。
5、读或写不能与I/O指令、加锁指令或串行化指令作重新编排。
6、读不能通过之前的LFENCE和MFENCE指令
7、写不能通过之前的LFENCE、SFENCE和MFENCE指令
8、LFENCE不能通过之前的读
9、SFENCE不能通过之前的写
10、MFENCE不能通过之前的读或写
(译者注:对于第6、第7条,可以根据下列代码来解释:
// LFENCE condition LFENCE mov eax, [edx] // SFENCE condition SFENCE mov [edx], eax
如果在加载指令之前有一条LFENCE指令,那么此加载指令不能在其上面的LFENCE指令完成之前被执行;对于存储指令也是同样,如果一条存储指令之前有一条LFENCE、SFENCE或MFENCE指令,那么此存储指令只有等那些Memory Fence指令完成后才能被执行。
而第8到第10可以用以下代码来解释:
// LFENCE condition mov eax, [edx] LFENCE // SFENCE condition mov [edx], eax SFENCE
其实这个描述就是发挥Memory Fence功效的示例。对于LFENCE,它可以保证在它之前的所有加载操作执行完之后,处理器方能往下继续执行;而对于SFENCE,它可以保证在它之前的所有存储操作执行完成之后,处理器方能往下继续执行。)
在一个多处理器系统中,应用以下次序原则:
1、单独的处理器使用与一个单处理器系统中相同次序原则(译者注:也就是说,当每个处理器都在独自运行时,每个处理器按照以上单处理器系统的次序原则运行程序)
2、一单个处理器的写,对其它处理器以相同的次序可观察到。
3、来自一单独的处理器的写不会根据来自其它处理器的写来做编排
4、存储器次序遵守因果关系(存储器次序遵守具有传递关系的可见性(译者注:所谓传递关系,在离散数学中是指,如果集合A对集合B有关系R;集合B对集合C也有关系R,那么如果关系R满足传递性,则集合A对集合C也有关系R。这里的意思就是说,如果处理器A对处理器B的存储可见,处理器B对处理器C的存储可见,那么处理器A对处理器C的存储亦可见。))。
5、任何两次存储以一个一致的次序被那些执行存储的以为的处理器可见。
6、加锁指令具有一个统括全局的次序。
见图8-1中的例子。考虑一个系统中的三个处理器,并且每个处理器执行三次写,对每一次定义了一个位置(A、B和C)。独立地看,处理器以相同的次序执行写,但因为总线仲裁以及其它存储器访问机制,三个处理器写各自存储器位置的次序,在每次相关的代码序列中处理器上执行时都可能不同。在位置A、B和C的最终值基于每次写序列的执行,都可能发生变化。
在本小节所描述的处理器次序模型实际上与奔腾和Intel486处理器所使用的一样。在奔腾4、Intel至强和P6家族处理器中仅有的增强如下:
1、添加了对投机读到支持,然而仍然要秉承上述的原则。
2、存储缓存转运,当一次读通过了对同一存储器位置的写时(译者注:即,当一次对某个存储器的写发生后,马上对此位置进行读,并且写的数据长度和读的数据长度完全一致,那么读可以不需要从此位置的存储器进行,而直接可以把要写的数据转运给读的目的寄存器。)
3、来自长的字符串存储以及字符串搬移操作的无序存储(见下面的8.2.4小节,“为字符串操作的无序存储”)
在P6处理器家族中,对目标为同一存储器位置的来自流存储的WC存储器的读的存储缓存转运并不会发生,参照勘误表。
8.2.3 说明存储器次序原则的例子
本小节提供了一组例子来说明在8.2.2小节中所介绍的存储器次序原则的行为。这些例子的设计是为了给软件编写者对存储器次序可能会如何影响不同指令序列的结果有一个更好的理解。
这些例子都限定在对被定义为可写回(WB)cache的存储区域的访问。(8.2.3.1小节描述了基于例子普遍性的其它限制。)读者应该理解,这些例子仅对软件可见的行为进行描述。一个逻辑处理器可以重新编排两次访问,即使其中一个例子指示它们不可能被重新编排。这样的一个例子只是为了陈述,软件无法探测到这样的一次重新编排会发生。类似的,一个逻辑处理器可以对存储器访问执行多于一次,只要对软件可见的行为与对存储器访问的一单次执行相一致。
8.2.3.1 假设、术语和注释
正如上面所提到的,本小节中的例子限定对存储器的访问是定义为可被写回(WB)cache的。它们仅应用于普通的加载、存储以及加锁的读-修改-写指令。它们不必应用于以下这些情况:对字符串指令的无序存储(见8.2.4小节);带有非暂时暗示的访问;通过处理器从存储器读,作为地址翻译的一部分(比如页遍历);通过处理器对段和页结构的更新(比如,更新“已访问”位)。
在本小节中以下例子的原则应用于单独的存储器访问以及加锁的读-修改-写指令。Intel 64存储器次序模型确保了,对于以下每条存储器访问指令,所构成的存储器访问操作呈现为以单次存储器访问来执行:
1、读或写一单个字节的指令
2、读或写一个字(2个字节)的指令,这个字的地址在一个2个字节边界处对齐
3、读或写一个双子的(4个字节)的指令,这个双字的地址在一个4字节边界出对齐
4、读或写一个四子的(8个字节)的指令,这个四字的地址在一个8字节边界出对齐(译者注:这个就是处理器在64位模式的情况下)
任一加锁指令(要么是XCHG,要么是其它加锁的读-修改-写指令)呈现出作为一个不可分割的并且不可被打断的加载后面紧跟存储的序列来执行,而不管对齐。
其它指令可能会以多次存储器访问来实现。从一个存储器次序的视点看,对于所构成的存储器访问是以哪种次序组成的,是不能保证的。此外,对一次存储所构成的操作是否与一次加载所构成的操作以相同的次序执行也是无法保证的。
8.2.3.2小节到8.2.3.7小节给出了使用MOV指令的例子。这些例子背后的原则应用于通常情况下的加载和存储访问,以及其它要从存储器加载或存储到存储器的指令。8.2.3.8小节以及8.2.3.9小节给出了使用XCHG指令的例子。这些例子背后的原则应用于其它加锁的读-修改-写指令。
本小节使用术语“处理器”来引用一个逻辑处理器。这些例子是用Intel 64汇编语法写的,并且使用以下助记协定:
1、以“r”打头的参数,诸如r1、r2被引用为寄存器(比如:EAX)
2、存储器位置用x、y、z表示
3、存储被写作为mov [_x], val,这暗示了val正被存储到存储器位置x中
4、加载被写作为mov r, [_x],这暗示了存储器位置x的内容正被加载到寄存器r。
正如前面所讲的,这些例子仅依赖于软件可见的行为。如果后面的小节中有这样的陈述:“两次存储被重新编排”,那么这仅仅意味着“从软件的角度来看,两次存储呈现出被重新编排”。
8.2.3.2 加载与存储均不与类似的操作作重新编排
Intel 64存储器模型不允许加载和存储与同种类型的操作重新编排。即,它保证了加载是以程序次序所见的,并且存储也是程序次序所见的。这可以由以下例子阐明:
例8-1:存储不与其它存储重新编排:
处理器0 处理器1
mov [_x], 1 mov r1, [_y]
mov [_y], 1 mov r2, [_x]
注:初始时,x = 0, y = 0;不管在什么情况下,r1 = 1并且r2 = 0是不被允许的。
只有处理器0的两次存储被重新编排或处理器1的两次加载被重新编排,这个不被允许的返回值才可能会被展现出来。
如果r1 = 1,那么对y的存储发生在从y加载之前。因为Intel 64存储器次序模型不允许存储被重新编排,所以之前的对x的存储发生在从y加载之前。因为Intel 64存储器次序模型不允许加载被重新编排,所以对x的存储也发生在后一个从x加载之前。得到,r2 = 1。
(译者注:以上的时序编排正好满足我们之前所讲过的传递关系的可见性。为了这里更清晰地为各位理清思路,我们可以从传递性角度来看。就那本段为例,如果r1 = 1,那意味着处理器0的mov [_y], 1已经执行完成,因此对于处理器1而言,处理器0的mov [_y], 1先被执行完成,然后再执行处理器1的mov r1, [_y];在对于处理器0中,按照程序次序,mov [_x], 1在mov [_y], 1之上,根据本小节的原则,必须在mov [_y], 1之前完成执行;因此,根据传递性规则,mov [_x], 1对于处理器1而言,必须也发生在mov r1, [_y]之前。)
8.2.3.3 存储不与之前的加载重新编排
Intel 64存储器次序模型确保了由一个处理器的一次存储不可以发生在由同一个处理器的先前一次加载前。这个由以下例子阐明:
例8-2:存储不与更老的加载重新编排
处理器0 处理器1
mov r1, [_x] mov r2, [_y]
mov [_y], 1 mov [_x], 1
注:初始时:x = 0,y = 0 ;r1 = 1并且r2 = 1是不被允许的
假定r1 = 1,
1、因为r1 = 1,所以处理器1对x的存储发生在处理器0从x加载之前。
2、因为Intel 64存储器次序模型防止存储与由同一个处理器的之前的加载重新编排,所以处理器1从y的加载发生在其对x的存储之前。
3、类似地,处理器0从x的加载发生在其对y的存储之前。
4、因此,处理器1从y的加载发生在处理器0对y的存储之前,得到结果:r2 = 0。
8.2.3.4 加载可以与之前对不同位置的存储重新编排
Intel 64存储器次序模型允许一次加载与之前对一个不同位置的存储重新编排。然后,加载不能与先前对同一位置的存储重新编排。
一次加载可以与之前对一个不同位置的存储进行重新编排的原因由以下例子阐明:
例8-3:加载可以与更老的存储重新编排
处理器0 处理器1
mov [_x], 1 mov [_y], 1
mov r1, [_y] mov r2, [_x]
注:初始时,x = 0,y = 0;r1 = 0且r2 = 0是被允许的
在每个处理器中,加载和存储都针对不同的位置,并因而可以被重新编排。这些操作的任意交叉执行都是被允许的。其中一种这样的交叉是在两次存储之前执行两次加载。这将导致每次加载的结果的返回值都是0。
一次加载不能与之前的一次对同一位置的存储进行重新编排的事实由以下例子阐明:
例8-4:加载不与对同一位置的老的存储进行重新编排
处理器0
mov [_x], 1
mov r1, [_x]
注:初始时,x = 0;r1 = 0不被允许
Intel 64存储器次序模型不允许加载与之前的存储做重新编排,因为它们是对同一位置的访问。从而r1 = 1必须被保持。
8.2.3.5 处理器内的转运被允许
存储器次序模型允许由两个处理器的并发存储被那两个处理器以不同的次序可见;特殊地,每个处理器可以感知其自己的存储发生在其它存储之前。这由以下例子阐明:
例8-5 处理器内转运被允许
处理器0 处理器1
mov [_x], 1 mov [_y] 1
mov r1, [_x] mov r3, [_y]
mov r2, [_y] mov r4, [_x]
注:初始时,x = 0,y = 0;r2 = 0且r4 = 0是被允许的
存储器次序模型没有规定两次存储以何种次序被两个处理器执行带有任何限制。这个事实允许处理器0在见到处理器1的存储之前先见到自己的存储,而处理器1在见到处理器0的存储之前先见到自己的存储。(每个处理器都保持自我一致性)这允许r2 = 0且r4 = 0。
在实践中,本例中的重新编排会发生存储缓存转运的结果。当一次存储被临时保持在一个处理器的临时缓存中时,它可以满足处理器自己的加载,但并不被其它处理器的加载可见(并且也不会去满足)。
(译者注:关于存储转运可以参考Intel官方的优化文档)
8.2.3.6 存储是传递可见的
存储器次序模型确保存储的传递可见性。具有因果性相关联的存储呈现给所有处理器的是,以因果关系的一致性发生。这可以由以下例子阐明:
例8-6 存储是传递可见的
处理器0 处理器1 处理器2
mov [_x], 1 mov r1, [_x]
mov [_y], 1 mov r2, [_y]
mov r3, [_x]
注:初始时,r1 = 0,r2 = 0,r3 = 0;r1 = 1且r2 = 1且r3 = 0是不被允许的
假定r1 = 1并且r2 = 1:
1、因为r1 = 1,处理器0的存储发生在处理器1的加载之前。
2、因为存储器次序模型防止存储与一次更早的加载被重新编排(见8.2.3.3小节),所以处理器1的加载发生在其存储之前。从而,处理器0的存储因果性地发生在处理器1的存储之前。
3、因为处理器0的存储因果性地发生在处理器1的存储之前,所以存储器次序模型确保了处理器0的存储以所有处理器的视点来看,呈现出发生在处理器1的存储之前。
4、因为r2 = 1,所以处理器1的存储发生在处理器2的加载之前。
5、因为Intel 64存储器次序模型防止加载被重新编排(见8.2.3.2小节),所以处理器2的加载按次序发生。
6、上述的各项暗示了,处理器0对x的存储在处理器2从x的加载之前发生,这意味着r3 = 1。
(译者注:本例中,也正好证明了处理器2中两次加载不能被重新编排)
8.2.3.7 存储以一致性次序为其它处理器所见
正如在8.2.3.5小节中讲解的那样,存储器次序模型允许由两个处理器的存储以不同的次序被那两个处理器所见。然而,任意两次存储必须对所有的处理器呈现出以相同的次序执行,而不仅仅是对那两个执行存储的处理器。这由以下例子阐明:
例8-7 存储以一个一致性的次序为其它处理器所见
处理器0 处理器1 处理器2 处理器3
mov [_x], 1 mov [_y], 1 mov r1, [_x] mov r3, [_y]
mov r2, [_y] mov r4, [_x]
注:初始时,x = 0,y = 0;r1 = 1且r2 = 0且r3 = 1且r4 = 0是不被允许的。
由8.2.3.2小节中讨论的原理:
1、处理器2的第一次和第二次加载不能被重新编排;
2、处理器3的第一次和第二次加载不能被重新编排。
3、如果r1 = 1且r2 = 0,那么对于处理器2而言,处理器0的存储在处理器1的存储之前出现。
4、类似地,r3 = 1且r4 = 0意味着对于处理器3而言(译者注:原文这边有些错误,这里已经更改),处理器1的存储在处理器0的存储之前出现。
因为存储器次序模型确保了任意两次存储对所有处理器(而不仅仅是执行那些存储的处理器)呈现出以相同的次序执行,所以这组返回值是不被允许的。
8.2.3.8 加锁的指令具有全局次序
存储器次序模型确保了所有处理器符合所有加锁指令的一单次执行次序,包括那些大于8字节的,或没有自然对齐的。这由以下例子阐明:
例8-8 加锁指令有一个全局次序
处理器0 处理器1 处理器2 处理器3
xchg [_x], r1 xchg [_y], r2
mov r3, [_x] mov r5, [_y]
mov r4, [_y] mov r6, [_x]
注:初始时,r1 = r2 = 1,x = y = 0;r3 = 1且r4 = 0且r5 = 1且r6 = 0是不被允许的
处理器2和处理器3必须遵循两次XCHG执行的次序。不失一般性地,假定处理器0的XCHG先发生:
1、如果r5 = 1,那么处理器1的XCHG交换到y在处理器3从y的加载之前发生。
2、因为Intel 64存储器次序模型防止加载被重新编排(见8.2.3.2小节),所以处理器3的加载按次序发生,并且从而处理器1的XCHG在处理器3的从x的加载之前发生。
3、由于处理器0的XCHG交换到x在处理器1的XCHG之前发生(根据假定),所以它在处理器3的从x的加载之前发生。从而,r6 = 1。
一个类似的参考(对处理器2的加载的引用)可以使用,如果处理器1的XCHG在处理器0的XCHG之前发生。
8.2.3.9 加载与存储不与加锁指令重新编排
存储器次序模型防止存储与加载与更早或更晚执行的加锁指令重新编排。本小节的例子只是阐明了一条加锁指令在一条加载或存储指令之前执行的情况。读者应该注意,如果加锁指令在一条加载或存储指令之后被执行,那么重新编排也会被防止。
第一个例子阐明了加载不能与不能与之前的加锁指令重新编排:
例8-9 加载不能与锁重新编排
处理器0 处理器1
xchg [_x], r1 xchg [_y], r3
mov r2, [_y] mov r4, [_x]
注:初始时,x = y = 0,r1 = r3 = 1;r2 = 0,r4 = 0不被允许
正如在8.2.3.8小节中所解释的那样,加锁指令的执行有一个全局次序。不失一般性地,假定处理器0的XCHG先发生。
因为Intel 64存储器次序模型防止处理器1的加载与之前的XCHG重新编排,所以处理器0的XCHG在处理器1的加载之前发生。这意味着r4 = 1。
一个类似的例子(对处理器2作为参照对象)可以被应用,如果处理器1的XCHG在处理器0的XCHG之前发生。
第二个例子阐明了,一个存储不可以与之前的加锁指令进行重新编排。
例8-10 存储不与锁重新编排
处理器0 处理器1
xchg [_x], r1 mov r2, [_y]
mov [_y], 1 mov r3, [_x]
注:初始时,x = y = 0,r1 = 1;r2 = 1,r3 = 0是不被允许的。
假定r2 = 1:
1、因为r2 = 1,所以处理器0对y的存储在处理器1从y的加载之前发生。
2、因为存储器次序模型防止一次存储与之前的一条加锁指令重新编排,所以处理器0对x的XCHG在其对y的存储之前发生。因而,处理器0对x的XCHG在处理器1从y的加载之前发生。
3、因为存储器次序模型防止加载受重新编排(见8.2.3.2小节),所以处理器1的加载按(译者注释性添加:程序)次序发生,从而处理器1对x的XCHG在处理器1从x的加载之前发生。因而,r3 = 1。
8.2.4 对字符串操作的无序存储
Intel酷睿2Duo、Intel酷睿、Intel奔腾4以及P6家族处理器修改了字符串存储操作期间的处理器操作(以MOVS以及STOS指令启动)来最大化执行性能。一旦“快速字符串”操作初始条件被满足(如下所述),从外部角度来看,处理器将实质上用Cache行模式对一个Cache行中的字符串进行操作。这致使处理器循环对源地址发布一条Cache行读,并对目的地址在外部总线上发布一次无效化,以通知目的Cache行中的所有字节将被修改,长度为字符串长度。在这个模式下,处理器将仅在Cache行边界接受中断。在这个模式下,目的(译者注释性添加:Cache)行无效化以及从而导致的存储可能将在外部总线上无序地被发布。
依赖于顺序存储次序的代码不应该对要存储的整个数据结构使用字符串操作。数据和信号量应该被分开。依赖于次序的代码应该在字符串操作后,唯一地使用一个离散存放的信号量以允许正确次序的数据被所有处理器可见。
“快速字符串”操作可以通过清除IA32_MISC_ENABLES MSR寄存器的快速字符串允许位(第0位)来被禁止。
“快速字符串”操作的初始条件是实现指定的。示例条件包括:
1、EDI和ESI对奔腾3处理器必须8字节边界对齐。EDI对奔腾4处理器必须8字节对齐。
2、字符串操作必须以升序地址次序被执行。
3、初始操作计数器(ECX)必须大于等于64。
4、源和目的的重叠,对于Intel酷睿2Duo、Intel酷睿、奔腾M以及奔腾4处理器而言不能少于一条Cache行(64字节);对于P6家族和奔腾处理器,则不能少于32字节。(译者注:这里的重叠就是指,源和目的字符串有一段叠交,那么源地址和目的地址的差的绝对值必须满足上述要求)
5、源和目的地址的存储器类型必须要么是WB(译者注:Write Back),要么是WC(译者注:Write Combined)。
8.2.4.1 基于写回(WB)存储器的对字符串操作的存储器次序模型
本小节将介绍基于Intel 64架构的写回(WB)存储器类型的对字符串操作的存储器次序模型。
存储器次序模型,遵守以下原则:
1、在一单次字符串操作内的存储可以被无序执行。
2、来自不同的字符串操作的存储(比如,来自连续字符串操作的存储)并不无序执行。所有来自一更早的字符串操作的存储将在来自一更晚的字符串操作的任一存储之前完成。
3、字符串操作不与其它存储操作重新编排。
快速字符串操作(比如以MOVS/STOS开始,并带REP前缀的字符串操作)可能会被中断或异常打断。中断是精确的,但可能被延迟。比如说,中断可能会在Cache行边界被接受,在每若干次循环之后,或对每若干字节操作之后。不同实现可以选择不同选项,或甚至可以选择不延迟中断处理,因此软件不应该依赖该延迟。当中断/陷阱处理到达时,指向下一个要被操作的字符串的源/目的寄存器,当存储在栈中的EIP指向字符串指令,并且ECX寄存器具有它所持有的跟在最后成功迭代之后的值。来自陷阱/中断处理的返回应该使得字符串操作从它所被打断的那点恢复执行。
字符串操作存储器次序原则(上述第2、第3项)应该被解释为,确认快速字符串操作的非可破坏性。比如,如果一个快速字符串操作在k次迭代之后被打断,那么由中断处理执行的存储,在快速字符串存储从第0到第k次迭代之后,从快速字符串存储在第k + 1次迭代之前将变得可见。
在一单次字符串操作之内的存储可以无序执行(上述第一项),只要快速字符串操作被允许。
8.2.4.2 阐明字符串操作的存储器次序模型的例子
下面的例子使用与8.2.3.1小节所描述的相同的注记和协定。
在例8-11中,处理器0通过rep:stosd做了一轮(128次迭代)双字字符串存储操作,将值1(EAX中的值)从位置x(保持在ES:EDI中),以升序的次序写到一个512字节的块中。从而,每次操作存储一个双字(4个字节),该操作被重复128次(存放在ECX中的值)。该存储块初始时包含0。处理器1正在读两个正被处理器0更新的部分存储块的位置,即读的位置在x到x + 511之间。
例8-11 在一个字符串操作内的存储可以被重新编排
处理器0 处理器1
rep:stosd [_x] mov r1, [_z]
mov r2, [_y]
注:初始时,在处理器0上:EAX = 1,ECX = 128,ES:EDI = x
初始时,[x]到511[x] = 0;x <= y < z < x + 512
r1 = 1且r2 = 0被允许
对于处理器1,可以感觉到这处理器0中的重复字符串存储正在无序地发生。假定,在处理器0上,快速字符串存储被允许。
在例8-12中,处理器0做了两轮独立的对128个双字存储的rep stosd操作,将值1(存放在EAX中的值)从位置x(被保持在ES:EDI中)按升序的次序写到第一块512字节中。然后它将1写到从x + 512到x + 1023的第二块存储块中。所有存储器位置初始都为0。存储块初始为0。处理器1从两个存储块执行两次加载操作。
例8-12 跨字符串操作的存储是不被重新编排的
处理器0 处理器1
rep:stosd [_x]
mov r1, [_z]
mov ecx, $128
mov r2, [_y]
rep:stosd 512[_x]
注:处理器0初始时:EAX = 1,ECX = 128,ES:EDI = x
初始时,[x]到1023[x]为0,x <= y < x + 512 < z < x + 1024
r1 = 1且r2 = 0是不被允许的
在上述例子中,对于处理器1,对在处理器0中的后面字符串操作(第二个512字节块),在看到更早的对第一个512字节块的字符串操作的存储之前是无法感知到任何存储的。
上述例子假定对第二个块(从x + 512到x + 1023)的写,在处理器0对第一个块的字符串操作已被打断时,无法获得执行。如果第一个块的字符串操作被打断,并且对第二个存储块的写在中断处理中被执行,那么第二个存储块的改变将在第一个存储块恢复之前将变得可见。
在例8-13中,处理器0通过rep:stosd做一轮(128次迭代)双字字符串存储操作,将值1(EAX中的值)从位置x(保持在ES:EDI),以升序的次序写入到一个512字节的块中。然后,它写到第二个存储器位置,在先前字符串操作的存储块外部。
例8-13 字符串操作不与后面的存储重新编排
处理器0 处理器1
rep:sotsd [_x] mov r1, [_z]
mov [_z], $1 mov r2, [_y]
注:处理器0初始时:EAX = 1,ECX = 128,ES:EDI = _x
初始时:[_x] = [_z] = 0,[_x]到511[_x] = 0,x <= y < x + 512
z是一个独立的存储器位置
r1 = 1且r2 = 0是不被允许的
处理器1不能感觉到由处理器0的后面的存储,直到它看到来自字符串操作的所有存储。例8-13假定处理器0对[_z]的存储在字符串操作被中断时,不被执行。如果字符串操作被打断,并且对[_z]的存储被中断处理执行,那么对[_z]的改变将在字符串操作恢复之前变得可见。
例8-14阐明了当一个字符串操作被打断时的可见性原则。
例8-14 被打断的字符串操作
处理器0 处理器1
// 在es:edi到达y之前被打断
rep:stosd [_x] mov r1, [_z]
// 中断处理
mov [_z], $1 mov r2, [_y]
注:在处理器0上初始时:EAX = 1,ECX = 128,ES:EDI = x
初始时:[_y] = [_z] = 0,[_x]到511[_x] = 0,_x <= _y < _x + 512,z是一个独立的存储器位置
r1 = 1且r2 = 0被允许
在例8-14中,处理器0启动一次字符串操作,从地址x开始写一个512字节的存储块。处理器0在k次存储迭代后被打断。此时,地址y仍然没有被处理器0更新。获得处理器0上控制的中断处理对地址z写。处理器1可以从中断处理看到对z的存储,在看到512字节的存储块的剩余存储当字符串操作恢复执行时之前。
例8-15阐明了伴随更早存储的字符串操作的次序。在先前所有的存储变得可见之前,来自一个字符串操作的任一存储都不能可见。
例8-15 字符串操作不与更早的存储重新编排
处理器0 处理器1
mov [_z], $1 mov r1, [_y]
rep:stosd [_x] mov r2, [_z]
注:对于处理器0初始时:EAX = 1,ECX = 128,ES:EDI = x
初始时:y = z = 0,x到511[x] = 0,x <= y < x + 512,z是一个独立的存储器位置
r1 = 1,r2 = 0不被允许
8.2.5 强化或弱化存储器次序模型
Intel 64及IA-32架构为强化或弱化存储器次序模型提供了若干机制,以处理特殊的编程情况。这些机制包括:
1、I/O指令、加锁指令、LOCK前缀、以及串行化指令迫使处理器上更强的次序。
2、SFENCE指令(在奔腾3处理器的IA-32架构中所引入)以及LFENCE和MFENCE(在奔腾4处理器中所引入)提供了存储器次序以及为存储器操作的特殊类型的串行化能力。
3、存储器范围寄存器(MTRR)可以被用于为物理存储器的特定区域强化或弱化存储器次序(见11.11小节)。MTRR只能在奔腾4、Intel至强,以及P6家族处理器中使用。
4、页属性表(PAT)可以被用于为一个特定的页或一组特定的页强化存储器次序(见11.12小节)。PAT只能在奔腾4、Intel至强以及奔腾3处理器中使用。
这些机制可以用作为以下方式:
受存储器映射的设备以及其它I/O总线上的设备经常对写到它们I/O缓存的次序敏感。I/O指令(IN和OUT指令)对如下这样的访问采取强写次序。在执行一条I/O指令之前,处理器以程序次序等待所有先前的指令完成并等待所有被缓存的写都写入到存储器。只有取指令和页表遍历可以通过I/O指令。后面指令的执行只有等到处理器确定I/O指令已经完成才能开始。
在多处理器系统中的同步机制可能依赖于一个强存储器次序。这里,一个程序可能使用一条锁指令,诸如XCHG或LOCK前缀,以确保存储器上的读-修改-写操作被自动执行。加锁操作一般像I/O操作那样操作,它们等待所有先前指令完成,并等待被缓存的写都写入到存储器(见8.1.2小节,“锁总线”)。
程序同步也可以用串行化指令执行(见8.3小节)。这些指令一般用于临界过程或任务边界以迫使所有先前的指令,在一次跳转到新的代码段或一次上下文切换发生之前完成。跟I/O与加锁指令一样,在执行串行化指令之前,处理器一直等待,直到所有先前的指令被完成,并且所有被缓存的写都写入到存储器。
SFENCE、LFENCE和MFENCE指令提供了一个高效的执行方式,以在产生弱次序结果的例程和消费数据的例程之间确保加载和存储的存储器次序。这些指令的功能如下:
SFENCE:在程序指令流中串行化所有在SFENCE指令之前发生的存储(写)操作,但不影响加载操作。
LFENCE:在程序指令流中串行化所有在LFENCE指令之前发生的加载(读)操作,但不影响存储操作。
MFENCE:在程序指令流中串行化所有在MFENCE指令之前发生的存储和加载操作。
注意,SFENCE、LFENCE和MFENCE比起CPUID指令来提供了一个更有效的控制存储器次序的方法。
MTRR在P6家族处理器中被引入,定义指定存储器区域的Cache特征。下面是用MTRR建立的存储器类型可以被如何使用以强化或弱化奔腾4、Intel志至强和P6家族处理器的存储器次序。
1、强非被Cache(UC)的存储器类型迫使对存储器访问的强次序模型。这里,所有对UC存储区域的读写呈现在总线上,并且不会执行无序或投机访问。这种存储器类型可以被应用于专用存储映射I/O设备的一个地址范围,以迫使强存储器次序。
2、对于弱次序可被接受的存储器区域,可以选择写回(WB)存储器类型。这里,读可以被投机执行,并且写可以被缓存或绑定。对于这种存储器类型,在原子(加锁)操作上执行的是锁Cache,这些操作不跨Cache行分裂,而这可以帮助减少与典型的诸如XCHG的同步指令相关联的使用,从而在整个读-修改-写操作期间锁总线所带来的性能处罚。对于WB存储器类型,XCHG指令锁Cache而不是总线,如果存储器访问被包含在一条Cache行内。
PAT在奔腾3处理器中被引入,以增强能够被指派到页或几组页的Cache特征。PAT机制一般用于在页层加强Cache特征,根据由MTRR所建立的Cache特征。表11-7展示了用MTRR对PAT的交互。
Intel建议为运行在Intel酷睿2Duo、Intel凌动、Intel酷睿Duo、奔腾4、Intel至强以及P6家族处理器所编写的软件假定为处理器次序模型,或更弱的存储器次序模型。Intel酷睿2Duo、Intel凌动、Intel酷睿Duo、奔腾4、Intel至强以及P6家族处理器并不实现一个强存储器次序模型,除了当使用UC存储器类型时。尽管奔腾4、Intel至强以及P6家族处理器支持处理器次序,但Intel并不保证未来的处理器将支持这个模型。为了使软件能移植到未来的处理器上,推荐操作系统提供基于I/O、锁、和/或串行化指令的临界区域以及资源控制构造和API,用于在多处理器系统中对共享存储区域的同步访问。软件也不应该依赖于处理器次序模型,在系统硬件不提供此种存储器模型的情况下。