一、 访存重排序
在并行多核系统中,访问顺序不一定一致。为提升性能,编译器或硬件往往会对指令序列进行 重排序(Recording) ,从而引入乱序排序(Out-of-Order Execution) 机制
1. 指令重排序三种类型
- 编译器优化导致的指令序列重排序。不改变程序语义的情况下,对指令重新安排语义。
- 指令级并行导致的指令序列重排序。现代高性能处理器采用指令级并行处理技术增加每个时钟周期执行的指令条数,从而提高处理器性能。如超标量指令技术通过处理器动态执行机制实现指令级并行,超长指令级技术采用编译器软件静态调度实现指令级并行,EPIC技术通过软硬件协作来提高系统性能。
- 内存系统引起的指令序列重排序。除了指令执行执行程序之外,通常还存在着内存系统感知到的内存访问顺序。处理器通常会采用Cache、读写缓冲区,使得程序运行时出现多核交互让Load和Restore看起来在乱序执行。
2. 三种不同的存储器访问顺序
1.程序顺序(Program Order):程序顺序是代码在特定处理器上运行时由代码本身给出的访存顺序,代表程序员预期的时间顺序。
2.执行顺序(Execution Order):执行顺序是指在给定处理器上运行时,特定的访存指令执行顺序。
3.感知顺序(Perceived Order):也称观察顺序。特定处理器感知到访存操作的程序。由于Cache访问、存储系统优化和系统互联操作本身以及其他存储器的访存操作顺序不一定相同,且架构也不一定相同。
某些情况下,程序正确性需要依赖内存访问顺序,而必须通过内存一致性模型加以规范。
二、 内存一致性模型
共享存储器上的多核系统上运行的必须要面对并行编程的问题。其中内存访问一致性需要软硬件协同配合,也就是软件与存储器之间的协议问题。
计算机系统层次结构上来看,计算机系统逻辑上是由裸机和不同层次的虚拟机构成的。内存模型分为软件内存模型(Software Memory Model)与硬件内存模型(Hardware Memory Model)两大类。
1. 软件内存模型
软件运行模型是一套关于程序员、编程语言、运行环境三者之间的一套协议。
软件内存模型是一种用于描述和模拟计算机内存系统的抽象模型。它可以帮助程序员更好地理解程序的内存访问行为,优化程序的性能,以及解决内存相关的并发问题。
软件内存模型通常包括以下几个方面的描述:
- 内存层次结构:描述内存系统的多层次结构,包括缓存、主存、磁盘等不同的存储层次。
- 内存访问语义:描述程序在内存中读取和写入数据的行为,包括读后写、写后读、置零等不同的内存访问语义。
- 内存一致性:描述不同线程对共享内存的访问应该具有一致性,即所有线程看到的内存状态应该是一致的。
- 内存排序规则:描述如何对内存操作进行排序,以确保多线程程序的一致性和正确性。
软件内存模型对于计算机系统的设计和优化具有重要的指导作用。在实际的软件开发过程中,程序员可以根据内存模型来优化程序的内存访问性能,以及避免内存相关的并发问题。同时,软件内存模型也是并发编程语言和并行计算领域的重要研究内容。
2. 硬件内存模型
处理器层次架构的内存一致性模型,可以理解为软硬件之间的一套协议
硬件内存模型是计算机硬件设计者为了实现多处理器系统和编译器优化而建立的内存一致性和访问行为的规范。它定义了程序在内存中的行为以及内存访问的可见性。
硬件内存模型通常包括以下几个方面的描述:
- 存储器一致性:描述不同处理器对共享内存的访问应该具有一致性,即所有处理器看到的内存状态应该是一致的。
- 内存访问语义:描述程序在内存中读取和写入数据的行为,包括读后写、写后读、置零等不同的内存访问语义。
- 内存一致序:描述对共享内存的访问应该遵循一定的顺序,以确保多处理器系统的一致性和正确性。
- 内存访问延迟:描述内存访问的时间延迟,包括读延迟和写延迟等。
硬件内存模型对于计算机系统的设计和优化具有重要的指导作用。在实际的计算机系统设计中,硬件设计者需要根据内存模型来优化内存访问性能,以及避免内存相关的并发问题。同时,硬件内存模型也是并发编程语言和并行计算领域的重要研究内容。
a.强一致性内存模型
强一致性内存模型(Strong Consistency Model) 是一种严格的内存一致性模型,它要求多处理器系统中所有对共享内存的访问都必须在一个一致的状态下完成。也就是说,在任何时刻,所有线程看到的内存状态都是一致的。
强一致性内存模型具有以下特点:
1. 所有内存访问操作必须在全局的顺序下完成,这个顺序对于所有线程都是一致的。 2. 如果一个线程对共享内存进行写操作,其他线程立即就能看到这个写操作的结果。 3. 如果一个线程对共享内存进行读操作,它只能看到在全局顺序中发生在读操作之前的写操作。
强一致性内存模型可以确保多线程程序的一致性和正确性,但是它也可能会限制程序的性能。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和正确性。
b.弱一致性内存模型
弱一致性内存模型(Weak Consistency Memory Model) 是一种较为宽松的内存一致性模型,它允许在对共享内存的访问上存在一定的时间延迟,并且允许不同线程看到的内存状态存在一定的不一致性。
弱一致性内存模型具有以下特点:
1. 对共享内存的访问操作可能存在一定的顺序错乱,即在一个线程看来,其他线程对共享内存的访问可能不是按照预期的顺序进行的。 2. 如果一个线程对共享内存进行写操作,其他线程可能不能立即看到这个写操作的结果,而是需要等待一段时间后才能看到。 3. 如果一个线程对共享内存进行读操作,它可能只能看到在之前某个时间点之前的写操作,而不能看到最新的写操作。
弱一致性内存模型相对于强一致性内存模型来说更加宽松,因此它可以提高系统的性能,但是它也可能会降低程序的一致性和正确性。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和正确性。
也就是说,强一致性内存模型能保证所有处理器、进程对数据的读取顺序保持一致,不能保证即为弱一致性内存模型
A. 顺序一致性内存模型
**顺序一致性内存模型(Sequential Consistency Memory Model)**是一种特殊的内存一致性模型,它要求程序在执行过程中,无论多少个处理器并行执行,无论程序是否同步,所有程序看到的操作执行顺序都是一致的。
顺序一致性内存模型具有以下特点:
1. 所有线程看到的操作执行顺序都是一致的。 2. 程序的执行结果与各处理器各自轮流执行后的结果相同,且各处理器内部的执行顺序由程序决定。 3. 所有操作都是原子的,即在执行过程中不会被其他线程打断。 4. 所有操作都具有立即可见性,即在一个线程中进行的操作会立即对其他所有线程可见。
顺序一致性内存模型为程序员提供了极强的内存可见性保证,但是它也可能会限制系统的性能。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和可见性保证。
Load(加载) 和store(存储)操作一致
直观简单的强一致性内存模型,有时也称强排序模型,但这种模型效率低下,不允许处理器为提升并行性乱序执行程序,所以现代高性能处理器大多硬件实现不适用这种模型。
因此在设计处理器内存模型时,多把顺序一致性内存模型作为理论参考性模型来做一定程度上的放宽,来优化模型提升性能。
最典型的放宽是对不同的加载、存储类型的放宽:
- 加载之后的加载重排序(Loads Reordered After Loads)
- 存储之后的加载重排序(Loads Reordered After Stores)
- 加载之后的存储重排序(Stores Reordered After Loads)
- 存储之后的存储重排序(Stores Reordered After Stores)
在多处理器系统中,不同的内存访问类型可能具有不同的放宽级别。其中,最典型的放宽是对不同的加载和存储类型的放宽。 加载操作(Load)是从内存中读取数据到寄存器中的操作,而存储操作(Store)是将数据从寄存器中写入内存中的操作。在多处理器系统中,加载和存储操作可能会受到不同的放宽限制。 具体而言,在某些内存一致性模型中,加载和存储操作可能会被放宽,这意味着它们可以在不同的处理器之间以不同的顺序执行,而不必保持严格的顺序。这种放宽可以提高系统的性能,但也可能导致一些并发问题。 例如,在某些内存一致性模型中,加载操作可能会被放宽到可以在存储操作之前执行,这被称为“加载优先”或“先加载后存储”模型。相反,在另一些内存一致性模型中,存储操作可能会被放宽到可以在加载操作之后执行,这被称为“存储优先”或“先存储后加载”模型。
B. 全存储排序内存模型
如果对顺序一致性模型的基础上放宽程序中的写、读操作顺序(允许写晚于相应读操作)则称为全存储(Total Store Ordering,TSO)内存模型,一般出现在处理器中增加了写缓冲区(Write Buffer,或简称Store Buffer)的情况下。
全存储排序内存模型是一种内存一致性模型,它要求在对共享内存的访问上必须遵循一定的顺序,即所有对共享内存的访问操作都必须按照一种特定的顺序进行。
全存储排序内存模型具有以下特点:
1. 所有对共享内存的访问操作都必须按照一种特定的顺序进行,这个顺序对于所有线程都是一致的。 2. 如果两个线程对共享内存进行操作,它们之间的操作顺序不能有交叉。 3. 如果一个线程对共享内存进行写操作,其他线程必须按照这个写操作所在的位置对共享内存进行读取,否则将无法获得正确的结果。
全存储排序内存模型可以确保多线程程序的一致性和正确性,但是它也可能会限制程序的性能。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和正确性。
E.g.:通过Cache写回(Copy-Back)方式以批处理模式刷新写缓存区,合并多次操作,减少总线占用,但可能与Load造成 乱序执行
且 每个处理器上写缓冲区是私有的可能造成 多核一致性问题
写缓冲区
写缓冲区
CPU0
Cache
总线互联的Cache
接总线互联的主存()
CPU1
上图:多核系统多级存储器架构
在该模型中,CPU0先收到写指令,然后再收到读指令。CPU0写入缓冲区之后,Cache缺失,但为执行完操作,写入操作结果可能未写入主存
全存储(TSO)排序模型只放写-读操作顺序,能保证所有Store指令之间的执行顺序与代码顺序一致。
C. 部分存储排序内存模型
在全存储(TSO)模型 的基础上进一步放宽写-写操作,就变为**部分存储排序(Partial Store Order,PSO)**内存模型。在这种模型中向存储区写入的指令如果存在地址相关性,仍能保持顺序执行,但不存在相关性的写写操作则允许乱序执行。
继续用这个图:
写缓冲区
写缓冲区
CPU0
Cache
总线互联的Cache
接总线互联的主存()
CPU1
上图:多核系统多级存储器架构
连续执行两次地址不相关写操作
部分存储排序内存模型是一种内存一致性模型,它允许在对共享内存的访问上存在一定的顺序错乱,但是要求在对共享内存的访问上必须遵循一定的顺序,即所有对共享内存的访问操作都必须按照一种特定的顺序进行。
PSO中,部分存储排序内存模型具有以下特点:第一次Cache缺失。第二次写命中,都先写入缓存区,但第二次会先执行完成,第一次要等待Cache(缓存)空间被填充数据才执行完毕,因此存在数据不一致的风险。
1. 对共享内存的访问操作可能存在一定的顺序错乱,即在一个线程看来,其他线程对共享内存的访问可能不是按照预期的顺序进行的。 2. 如果两个线程对共享内存进行操作,它们之间的操作顺序不能有交叉。 3. 如果一个线程对共享内存进行写操作,其他线程必须按照这个写操作所在的位置对共享内存进行读取,否则将无法获得正确的结果。
部分存储排序内存模型相对于全存储排序内存模型来说更加宽松,因此它可以提高系统的性能,但是它也可能会降低程序的一致性和正确性。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和正确性。
D. 宽松内存顺序内存模型
如果把读-写、读-读 操作也进一步放开,只要与地址无关的指令都可以乱序执行。
宽松内存顺序内存模型(Relaxed Memory Order,RMO) 是一种内存一致性模型,它允许程序员在内存访问上拥有更大的自由度,但同时也要求程序员必须显式地标记出对内存的访问操作。
宽松内存顺序内存模型具有以下特点:
- 程序员必须显式地标记出对内存的访问操作,例如使用volatile关键字或memory barrier指令。
- 内存访问操作可能会在不同的线程之间出现顺序错乱,但是它们必须遵循一种特定的顺序,即所有对共享内存的访问操作都必须按照一种特定的顺序进行。
- 如果一个线程对共享内存进行写操作,其他线程必须按照这个写操作所在的位置对共享内存进行读取,否则将无法获得正确的结果。
宽松内存顺序内存模型相对于其他内存一致性模型来说更加宽松,因此它可以提高系统的性能,但是它也可能会降低程序的一致性和正确性。因此,在实际的系统设计中,需要根据实际情况选择合适的内存模型来平衡性能和正确性。
由此可见,从 顺序一致性内存模型 到 全存储排序内存模型,再到 部分存储排序内存模型 和 宽松内存顺序内存模型。保持顺序一致性的能力是由强减弱的
很多硬件相关的强一致性顺序模型的强制禁止乱序是没有必要的
而弱一致性模型,保证其一致性的责任就落到了程序员手上。
几种主流处理器架构:
- 早期DEC公司的Alpha服务器,比较严格的硬件弱内存模型。
- 类似PowerPC、ARM和安腾(Itanium)弱内存排序(Weak Memory Ordering,WMO) 的内存模型(比Alpha增加了数据依赖性顺序)。
- 应用广泛的Intel 64 (x86-x64)架构使用过程一致性(Process Consistency )内存模型,基本属于强一致性顺序模型。
- 有些处理器架构支持多种内存一致性模型。如RISC-V处理器默认内存弱内存排序模型,也可使用全存储排序内存模型。
三、 内存屏障指令
内存屏障指令(Memory Barrier Instruction) 是一类同步屏障指令,用于确保在对内存的访问操作中,先发生的操作(包括读写操作)在后发生的操作之前执行完毕。
内存屏障指令可以保证不同线程之间的内存访问操作的顺序性,从而确保多线程程序的正确性。
现代计算机系统中,有多种类型的内存屏障指令,例如:
- 内存栅栏(Memory Barrier):是一类同步屏障指令,用于确保在对内存的随机访问操作中的一致性。
- 指令栅栏(Instruction Barrier):是一类同步屏障指令,用于确保在对内存的随机访问操作中的一致性。
- 内存栅障(Memory Barrier Primitive):是一类同步屏障指令,用于确保在对内存的随机访问操作中的一致性。
这些内存屏障指令的具体实现和使用方法因不同的计算机系统而异,需要参考相关的硬件和操作系统文档进行了解和应用。
以下用PowerPC、Power处理器的三条同步指令为例
1. 输入输出控制指令eieio
Enforce In-order Execution of Input/Output(强制按顺序执行输入/输出操作),也即在前面所有load和store指令执行完毕之后再执行后续的指令。
//下面为一段向外设发送两段数据的代码 1. while(TDRE == 0); //TDRE表示映射到另一个内存地址的状态寄存器 2. TDR = char1; //TDR表示内存空间的发送数据缓冲器 3. asm(" eieio"); //加入eieio指令会强制执行上一行再执行下一行 4. while(TDRE ==0); //取0发射器缓冲器不空 5. TDR = char2; //确保程序执行不出错
2. 同步指令sync
sync指令功能为等待所有前序操作执行完毕。
PowerPC架构中,定义了执行同步(Execution Synchronizing)概念:如果某条指令i导致指令分发暂停,并且只有当所有正在执行的所有指令都已执行完成i并报告了触发的异常时,指令才算执行完毕。
而sync指令除了执行同步,还要等待所有被挂起的内存访问结束,并且发出一个地址广播周期。可见执行这条指令的性能代价很大。
使用比如让处理器进入低功耗模式使用sync指令。(寄存器编程)
//进入低功耗模式的伪代码 1. asm(" sync");//等待所有被挂起的内存访问结束 2. //进入低功耗模式 3. asm(" sync");
3. 同步指令isync
isync指令覆盖了sync指令的功能,并且在等待所有前序指令执行完毕的同时还清空指令队列,也即按照新的处理器上下文重新加载指令队列。处理器把这种指令的操作称为指令上下文同步(Instruction Context Synchronizing)。系统调用指令、中断返回指令都需要指令上下文同步。
举例而言,当使用写操作指令激活指令 Cache 时,指令队列中可能己经存在若干指令了,此时先执行 isxne 指令就会让后续指令进人指令 Cache。
此外,Power 处理器还有两条专用于多处理器间共享资源同步指令 lwarx 和
stwcx.
> lwarx(Load Word and Reserve Index ):这个指令用于读取内存中的某个字(32位)数据,并将其存储到某个寄存器中。更重要的是,它在读取数据的同时,会在内存中的某个特定位置(通常是一个地址)设置一个"reservation"标记,表示该资源已被一个特定的处理器"预定",其他处理器在尝试读取或修改这个资源时将会失败。这个指令通常用于实现资源访问的互斥,即一次只有一个处理器可以访问或修改某个资源。 > stwcx(Store Word Conditional Index):这个指令用于将某个寄存器中的字数据写入到内存中。如果写入操作成功,且内存中的"reservation"标记仍然存在,那么这个写入操作就会失败,并且stwcx指令会设置一个条件代码,表示该资源仍然被其他处理器"预定"。这个指令通常用于检查资源是否已经被其他处理器锁定,如果已经被锁定,那么当前处理器就会停止进一步的访问操作,避免产生冲突。
每种处理器架构都有其自身定义的内在屏障指令,以支持其内存一致性模型。这对需要在不同平台之间迁移程序是个挑战。而类似Java 语言这样的软件开发环境在往限制程序员直接使用内存屏障,而是要求程序员使用互斥 原语实现同步访问。
在Java中,程序员可以使用一些内置的同步原语,如synchronized关键字和java.util.concurrent包中的各种锁和并发工具,来实现对共享资源的互斥访问。这些原语在内部使用了底层的内存屏障和其他的硬件特性来确保线程安全。