4.2.2 内存屏障
超标量体系结构中,CPU引入了并行性。在一个CPU上可能有几个同类的功能单元(译注:以此来实现并行),并且带有额外的电路将指令分发到这些功能单元。例如,大部分的超标量设计都包含了多个算术逻辑单元(arithmetic-logical unit)。分发器(dispatcher)从内存读取指令然后决定哪些可以并行执行,接着将他们分发到那两个单元(译注:从上下文完全看不出来是哪两个?个人猜测指的是上文提到的多个算术逻辑单元)。分发器的性能是超标量体系设计整体性能的关键:这些单元的流水线应该尽可能的饱和。因此,一个超标量CPU的分发器总是重排指令来获得最佳的吞吐量(译注:每个时钟周期内分发尽可能多的指令)。加载、保存操作也不例外。
例如,想象一个程序对两个内存中的整数做加法。当第一个参数从内存而不是高速缓存中获取,第二个参数从高速缓存中获取时,第二个加载操作很有可能先于第一个完成。与此同时,第三个加载操作可以执行了(译注:需要三个加载操作,根据文中提到的load/store指令推断出可能指的是RISC的三操作数加法)。分发器使用内部锁来禁止加法在所需的加载操作完成前执行。
而且,大部分现代CPU支持(译注:原文写做sport,疑有误)使用少量的寄存器存储几个保存操作的结果,以便在之后的某个时间同时执行(译注:以此提高效率),这些寄存器称作保存缓冲(store buffer)。它们可以缓冲顺序保存或者对超标量CPU而言的乱序保存。简而言之,只要加载或者保存操作不访问之前保存的同一个内存字(或者反之),它们可以以任意顺序在CPU上执行。
要保证在SMP上代码的正确性,还需要进一步的措施。2.1节中的简单原子链表代码可能像图7表示的那样执行。
这种方法需要new节点的next指针在插入链表头之前初始化。如果这些指令乱序执行,链表就会在第二条指令完成前处于不一致状态(译注:指的是图7左边的代码,此时i->next指向了新节点,而new->next并没有指向原i->next的那个节点,链表结构会处于不一致状态),而在此期间,其他CPU可能会遍历链表,(译注:执行插入操作却没有完成的那个)线程也可能被抢占,诸如此类。
并不需要这两个操作一条接一条的准确执行,仅仅需要第一条比第二条先执行。为了强制在指令流水中的所有读、写操作在后面的指令取指前结束,CPU有被称为内存屏障的指令。我们需要区分读内存屏障(在所有的读操作结束前等待)、写内存屏障(在所有的写操作结束前等待)和内存屏障(在所有的读、写操作结束前等待)。正确的代码应该这样写:
重排指令可以导致临界区的操作“渗出”(bleed out,译注:即某些临界区操作在指令重排后放到了临界区的外面),见图8。
应该在临界区内部的那行代码(译注:注释为in critical section的那行)显然不在临界区,因为释放锁的操作先执行了。另一个CPU可能会改变部分数据,导致不可预料的结果。
为了防止这样的问题,我们需要再次修改锁操作的代码。注意,并不需要防止临界区前的读、写操作“渗入”临界区(译注:意思是不需要在lock的代码加入mb防止不属于临界区的代码在指令重排后进入临界区)。显而易见,分发器就不会优化(override)逻辑指令流(译注:意思是指令重排技术不会处理跳转类的指令,因为这显然会改变程序的逻辑),所以临界区的代码只会在CPU退出while循环后执行。
内存屏障会刷新保存缓存并将流水线停下来,然后执行所有未决的读、写操作,所以它对多个流水线和功能单元有负面作用(译注:原文:so it negatively impacts the performance proportional to the number of pipeline stages and number of functional units.)。这就是之前的表格中的内存屏障操作如此耗时的原因(译注:第4节 性能研究中的图)。原子操作需要如此长的执行时间是因为他们同样刷新保存缓冲来立即执行(译注:不然它们也会被存储到保存缓冲里)。
阅读(3486) | 评论(0) | 转发(0) |