并行编程在近些时候特别火爆,因为ILP得潜力已经被发掘得差不多了,TLP必然成为未来提高微处理器性能的最重要方向,最重要的体现形式就是多核并行处理器,看看做GPU的NV前些日子的嚣张就知道多核并行运算的炙热程度。现在我们就来看看ILP到TLP的转换中,我们程序员面临哪些可能的挑战。以下内容很多自己推测,肯定存在错误,仅仅作参考。
对于程序员来说,并行运算最重要的地方就是共享资源正确和高效的使用,而程序员所能最大限度掌控的便是存储系统。我们来看看INTEL多核CPU的构成原理图:
Core0 Excution > RFs > L1 Cache > L2 Cache > Memory > (Buffer) External Storage(HD)
Core1 Excution > RFs > L1 Cache >
其中红色部分为共享资源,下面我来详细讨论以下几个问题。
1. Memory的使用策略
INTEL CPU对内存的使用策略多种多样,分别用来满足不同的应用需求
A. 强不可缓冲模式(strong uncacheable)
B. 不可缓冲模式(uncacheable)
C. 写透模式(write through)
D.写回模式(write back)
E. 写组合(write combining)
F. 写保护(write protection)
我们常见的的两样就是D和E,普通应用程序执行的内存模式都是WB的,AGP内存是WC的,而内存使用模式的定义可以通过PAT,MTTRs等进行配置,详细情况请参考相关手册。INTEL CPU按访问模式将数据分为3类:临时的,表示在不久马上就会用到的;连续的,表示数据会被序列访问的;非临时的,表示接下来一段时间不会被再次用到。WB内存模式最主要针对第一种使用模式的数据设计,经常被反复读写,所以WB模式会完整利用Cache来加速这样的访问。而WC模式主要是针对写的非常多,而基本不怎么读的数据,比如AGP MEMORY,这样数据就不必经过Cache缓冲,经过Cache反而会影响效率,因为同时要写Cache和内存,造成不必要的cache污染和cache波动。
对于WB模式内存区域具备如下特性:
1、 读取内存数据是投机执行的,不要指望内存读命令按程序顺序执行
2、 读可以提前到写之前执行,比如因为总线忙,写被BUFFER到STORE BUFFER中,而后续的读已经被CACHE,则读可以先进行。
3、 写内存通常都是遵循程序顺序的,但CLFLUSH和NON-TEMPORAL指令除外,因为这些指令是WC语义的,WEAKING ORDER,NT写被BUFFER在WC BUFFER中(通常为64BYTE)。
4、 写可被缓冲起来(CACHE中或者STORE BUFFER中)。
5、 WB内存绝对不会实行投机写。
6、 可执行存储转发(STORE FORWARDING)
7、 读和写内存都不会被提前到I/O指令、LOCK指令和序列化指令之前。
8、 读不能提前到LFENCE和MFENCE之前。
9、 写不能提前到SFENCE和MFENCE之前。
2. Cache的原理
INTEL CORE2实现了L1和L2两级CACHE,L2双核共用,CORE2的2级CACHE都是64B/LINE,共1024行。分别是8路和16路组相连,CACHE在与主存之间传输数据采用8-Transfer Burst transcation,(CACHE LINE不支持partial write transaction)这个是总线技术术语,什么意思呢?就是一次传输64字节,分8组突发传送,为原子操作(毕竟我不是专业做硬件的,解释可能有误,详细请参看总线技术相关文档),即便只需要小于64字节的数据,总线依然传送64字节填充整个CACHE LINE。根据CPU的档次,L2 CACHE大小不一,但L1都是64KB的,32的指令,32的数据。两个Core的L1d Cache之间可以互相传输数据(可能与MESI协议实现有关),L1 Cache拥有几个数据和指令硬件预取器,L2 Cache的预取器是基于L1 Cache预取器的访问模式和密集程度来工作的,使用了一个改进的Round-Robin算法动态的在两个处理器之间分配带宽。前端总线接口也采用了类似的方式以确保平衡。L1 Cache和L2 Cache采用了独立访问设计,也就是说Core可以直接从L2 L1或Main Memory中直接取数据,无须逐级上升。intel cache使用了mainly inclusive设计。
现在我来详细讨论CACHE LINE的构造,如下图:
Data blocks Tag Index Displacement Valid bit
DATA BLOCKS包含实际从主存中攫取的数据64B,TAG\INDEX\DISPLACEMENT均为内存地址,valid bit 好像是2bit用来表示此LINE的状态,后面我会详细叙述。可能朋友们看到这里对组相联这个概念还有些许模糊,其实这是对于CACHE的使用策略,我们知道CACHE远小于内存,如何合理高效的使用CACHE是个大问题,常用的算法有如下3中:
全相联:内存中的任何64对齐字节可以放在任何一CACHE LINE中
直接映射:内存的任何一个64对齐字节只能放在特定的一CACHE LINE中
N路组相联:内存的任何一个64对齐字节只能放在某些(N)CACHE LINE中
L1为8路组相联,表示每8条CACHE LINE对应128个组值中的一个。7BIT的组值,6BIT的INDEX值,19BIT的TAG。很显然如果L1D为32KB并且为8路组联,则每128个64组组织重复一次。
我们详细理一下INTEL CORE2 L1 CACHE的参数:64KB、64B/LINE、8路组相联。所以我们可以知道CACHE LINE数目为1024行,每8行编为一组,共128组。CACHE分TAG SRAM和DATA SRAM,每行都有一个TAG值,假设为所存数据物理地址的高19位,如下图TAG SRAM的组织:
Set Index Way0 Way1 Way2 Way3 Way4 Way5 Way6 Way7
0 Tag
1
…
127
CORE2可以一直读取8路TAG同时比较。INDEX索引很显然是物理地址低位6BIT截0(64),再取128的模,所以应该是[12:6]位为组索引,然后用要操作的地址的高19位与TAG SRAM中的128组中的每个值比较,一旦比较成功则CACHE HIT,这样我们便知道数据在哪组哪路。接下来便是数据的实际操作,L1 CACHE DATA SRAM好像是8BYTE 数据读宽度,一次可以READ 8路,还记得系统总线的8 TRANSFER BURST TRANSCATION吗?估计跟这也有关系.
上图可能有点问题(其实细节跟SRAM实现细节有关),但我们简单考虑,我们知道了HIT的SET数,以及SET中的路数,所以我们知道CACHE LINE为SET×8(way)+ Set_Offset,结合Data offset可以读取正确的数据出来。CORE2 使用CACHE的方式,我不知道具体情况,只知道INTEL CORE2 CACHE替换策略为PSEUDO-LRU算法,并且高端处理器CACHE HIT都使用虚拟地址完成,而不是物理地址,减少地址翻译负载。
3. 存储一致性维护
在此必须首先对CORE2双芯架构有个了解,为什么称其为双芯而不是双核,主要是因为CORE2 DUO的两个核是通过前端总线连接在一起的,导致不光CPU与外部设备访问要使用FSB,甚至CORE之间信息交流(MESI协议的实施)也要占用带宽,这是CORE2架构中最鸡肋的部分,当然在David Kanter以前的文章中提到L1 CACHE之间可以直接通信,可能是INTEL做了相关优化,但具体不得而知,暂时我们就当CORE只能通过FSB互相交互。每个CACHE LINE有一种状态,可以分别为MODIFIED,EXCLUSIVE,SHARE和INVALID,MODIFIED默认就是EXCLUSIVE的,一般MESI协议会定义若干种同步消息来交流信息,而CORE都会在总线上监听SNOOPING这些消息,然后对自身的特定CACHE LINE的状态做修改。必须重点提醒的是所谓存储一致性主要是针对CACHEABLE MEMORY提出的,比如WT和WB,而WC模式是无须硬件保证存储一致性的。以下是MESI协议中CACHE LINE根据自身状态和读写情况,进行自我状态改变,总线同步消息发送以及内存操作的图例,对理解MESI协议的实现非常重要。(贴不上图,随后补上).
4.程序编写中需要注意些什么
如何保证我们对存储器的访问是原子的?
很显然并行程序设计少不了进行同步操作,对临界区、时间、信号等同步资源的访问就必须是原子的。CPU提供了3大类硬件功能供操作系统开发人员和应用程序开发人员来保证最基本的原子操作:1、硬件上真正不会被打断的指令。2、系统总线锁。3、CACHE一致性管理。接下来我详细解释下以上3个功能。CORE2提供了一些保证不会被中断的指令,比如读或写位于对其位置的1/2/4/8字节长内存,如果内存没有被CACHE,一次总线2字节的数据传输的访问也是原子的,CACHE LINE内数据访问都是原子的。因为CORE2的数据总线是4字节的,而访问指令只支持1、2、4字节访问,假设要访问的内存并没有对齐,就很可能需要2次或2次以上总线数据传输才能完成反问,这样就又可能被打断了,比如访问位于地址3上的2个字节,这就需要2次总线操作,但如果访问的2个字节位于地址1上就只需要一次,你可能会问,这明明没有对齐,怎么能一次传输搞定呢?因为这是CPU会利用总线宽度冗余,一次传输从0~3地址的4个字节,这样其中需要的1、2字节也包含在里面了,但位于3上就不行了,因为不论总线是2字节还是4字节传送,都无法一次传输完毕,这与系统总线数据传输特性有关,只能对齐传送数据!新人可能还会问个问题?操作寄存器的指令是否能被打断呢?我认为是不可以的,因为CISC指令的CPU将一条指令当作完整功能,能打断的只是外部操作,比如数据LOAD和STORE,CPU内部一条指令解码后的所有微指令执行之间应该是无法被打断的,因为只有当一条指令的所有微指令都retirement后,CPU的状态才算更新完毕。除非掉电,当然这个只是我的猜测,没有看到官方文档上有说明,知道的朋友可以告诉我。但这个问题对于并行运算无关紧要,因为寄存器是CPU内部私有的,不是共享资源。总线锁,CPU有一个LOCK针脚,会触发LOCK信号,用来锁定系统总线(主要就是前端总线)资源不被其他总线主控逻辑(如另外的核和BUS AGENT)占用,当一个核LOCK住系统总线后,其他核或总线代理都必须等待释放,这时外部中断什么的都无法传送到CPU中来。LOCK可以将外部储存器访问原子化(因为其他主控逻辑都无法使用总线反问内存了),但细心的朋友可能会意识到一个问题LOCK只是用来锁定外部的系统总线,应该不能用来保证跨CACHE LINE的内存访问吧,是的你想对了,我们无法保证这种操作的原子性,因为很显然CACHE是在CPU内部,不受LOCK信号影响,CORE2中如果访问CACHE LINE的指令前加了LOCK前缀,是不会触发LOCK信号锁定系统总线的,而是会使用MESI协议保证CACHE一致性。任何需要1次以上存储访问的操作都不是原子的,很可能被打断和中间安插其他访问指令,比如add [mem], eax,其实至少有2次存储访问,一次读,一次写,在SMP系统中就很可能在这2次存储访问之间加入其他存储指令,为了安全需要加LOCK前缀来保证一条指令存储访问的原子性,这又涉及2种情况,CACHE内语义和总线语义,如果访问数据已经CACHE在高速缓冲中,则CORE2不会在总线触发LOCK信号锁定总线。非对齐数据的存储访问也是非原子的,因为需要1次以上的存储访问,这就是为什么SMP系统中内存访问指令需要加LOCK的原因,如XADD和CMPXCHG指令都需要多次存储访问,访问其他CORE同时访问数据,必须LOCK。而单处理器上我觉得XADD,CMPXCHG之流应该无需加LOCK,注意这里的非原子有两种情况,一种是但处理器一种是多处理器,在单处理器上一条指令的执行是原子的,不论他需要访问几次存储器,RETIREMENT标志着一条指令的执行完毕,指令的RETIREMENT是指令顺序的,但执行不一定是。所以单处理器不存在同时指令多条指令的问题
_BEGIN:
PAUSE
Cmp [sem], 0 //测试锁
JNE _ BEGIN
Mov [sem], 1 //得到锁
DO SOMETHING
Mov [sem], 0 //释放锁
很显然以上程序是错误的,因为
Cmp [sem], 0 //测试锁
JNE _ BEGIN
Mov [sem], 1 //得到锁
中间是可以被打断的,多个线程轻松的可以同时得到锁。我们需要一条原子操作既完成比较又完成赋值,让我们看看cmpxchg,
Mov eax, 0
_BEGIN:
PAUSE
Cmpxchg [sem], 1
JNE _BEGIN
DO SOMETHING
Mov [sem], 0
很显然没问题,只要有其中一个线程进入,其他的线程都会碌碌无为在前面循环,但这只是在单核CPU上有效,对于多核可就没这么幸运了,因为多核上可能同时执行Cmpxchg [sem], 1这条指令,这条指令至少有2次存储访问(不对齐可能就会有4次访问),一次是读sem,一次是写sem,不论sem在内存还是在cache中,2个核中的4次访问可能互相交叉,A READ SEM,B READ SEM, A WRITE SEM, B WRITE SEM,很显然错了。如何避免这种情况呢,便是加前缀LOCK,触发总线锁定信号,这样其他核必须等待总线释放后才能访问特点内存地址,对齐数据访问都是原子的,跨DATA BUS宽度、CACHE LINE和页边界的都很可能不是原子的。显然不是原子的情况都是需要1次以上存储访问的,不管是CACHE还是总线。
在TEMPORAL语意的MEMORY区域上执行NON-TEMPORAL STORE是否需要程序保证数据一致性?
回答是肯定的,单独提出这个问题的主要原因是存在大量的NON-TEMPORAL STORE优化,主要是MOVNTQ等指令,这些指令是NON-TEMPORAL语意的,跟WC BUFFER一样,使用write combining buffer进行写缓冲,并且写是WEAK ORDERING的,顺序和结果都会在此BUFFER中合并,如果FSB是64BIT的,一般WC BUFFER也是64字节,分8个CHUNK,通过FSB的8-Transfer Burst transcation一次(原子的)将数据传输到内存控制器,如果WC BUFFER并没用写满,则需要通过多次(非原子的)partial write transaction完成。对WB内存进行NON-TOMPORAL STORE需要注意的问题,对于已经处于CACHE之中的内存,则遵循WB语义,如果不在则遵循WC语义。SFENCE不见得能把NT STORE的数据FLUSH到内存中,SFENCE貌似只对WC BUFFER和STORE BUFFER中的有用,这就是所谓的全局可见性,在CACHE和内存中都具备全局可见性,而CORE内部的REGISTER FILE都不是,这三条指令具备执行屏障和使前面存储访问指令全局可见的效果。
Core2中到底有哪些缓冲?
1、L1(I/D) L2 L3 CACHE
2、TLBs(I/D)translation lookaside buffer
3、Loop Buffer
4、Store Buffer
5、Write Combining Buffer
注意Trace Cache只有NetBurst架构的微处理器才具备,CORE2没有。
5.未来发展趋势
离我们最近的就是Nehalem架构的CPU CORE I7,应该说相当的先进,SHARE BUS 的FSB从此消失,迎来的是QPI互联的原生4核,集成内存控制器,PCI-E控制器和IOMMU单元,北桥即将消失,甚至连GPU集成也已经可在INTEL技术线路图中出现,对于底层程序员还是会有一定影响。详细请参看 《Inside Nehalem: Intel's Future Processor and System》。