2005年,Herb Sutter发表了他的开创性文章“免费午餐结束了”(Sutter,2005)。他概述说,微处理器的顺序性能将很快停滞不前,业界将通过增加内核数量来提供更高性能的处理器作为回应。这种范式转换的结果是从编写软件的纯顺序编程模型转向具有多个执行线程的并发编程模型。当应用程序固有地表现出并行性时,当线程在不同的核心上执行时,在多个线程之间分配工作可以产生性能提升。
多线程并发有两个主要缺点-(1)同步开销,例如锁的串行化性质,以及(2)编程、调试和验证的困难。在描述不同的同步策略(例如,粗粒度锁定、细粒度锁定和无锁算法)时,这两个属性之间通常存在负相关。
硬件事务存储器(HTM)允许两个或更多线程安全地执行临界区。事务-并行,不使用诸如互斥锁之类的序列化原语。事务以原子性、一致性和隔离性的属性推测性地执行(Harris等人,2007年),微体系结构负责检测竞争条件并从竞争条件中恢复。例如,如果一个线程写入另一个线程已读取的内存位置。这可以通过更简单的编程模型提供更高的并行性。
事务内存扩展(TME)计划是ARM的A-Profile未来架构技术计划的一部分,该计划提供有关该架构的未发布版本的高级信息。TME是一种尽力而为的HTM架构,它不保证事务的完成。程序员必须提供后备路径来保证进度,例如互斥保护的临界区。它提供了强隔离,这意味着事务与其他事务和并发非事务性内存访问都是隔离的。它使用事务的扁平嵌套,其中嵌套的事务包含在外部事务中。在外部事务提交之前,嵌套事务的影响不会对其他观察者可见。当嵌套事务中止时,它会导致外部事务(及其内部的所有嵌套事务)中止。
TSTART<;xd>;8-此指令开始新事务。如果事务成功启动,则目标寄存器设置为零,处理器进入事务状态。如果事务失败或被取消,则以事务方式执行的所有状态修改都将被丢弃。然后,向目标寄存器写入编码故障原因的非零值。
TCOMMIT-此指令提交当前事务。如果当前事务是外部事务,则退出事务状态,并且将以事务方式执行的所有状态修改提交给架构状态。
TCANCEL#<;imm&>-此指令退出事务状态,并丢弃所有以事务方式执行的状态修改。在外部事务的TSTART指令之后的指令处继续执行。外部事务的TSTART指令的目标寄存器用TCANCEL的立即操作数写入。
Ttest<;xd>;t-此指令将事务深度写入目标寄存器,否则值为0。
图2:说明了四条TME指令的语义。(1)显示了创建和提交事务的两个线程。线程T1与线程T2冲突,因此T2中止其事务并回滚到TSTART指令。T2';的TCOMMIT账户从未联系到。(2)线程T1创建一个事务,但使用TCANCEL手动中止它。它将中止并回滚到TSTART。T1的TCOMMIT请求从未到达。(3)线程T1创建并提交测试事务深度的嵌套事务。当在外部事务中执行Ttest时,它返回1,而如果在内部事务中执行,它将返回2。
Gem5是一个开源的系统和处理器模拟器,广泛用于学术界和工业界的计算机体系结构研究。Gem5提供两种可供选择的存储系统-经典存储系统和Ruby存储系统。The Classic Memory System是从Gem5的前身M5继承而来,并实施了MOESI一致性协议(Sorin,Hill,&;Wood,2011)。相比之下,Ruby Memory System提供了一种灵活的方式来定义和仿真自定义缓存一致性协议,包括一系列互连和拓扑。协议是使用特定于域的语言SLICC(包括高速缓存一致性的规范语言)通过状态、转换、事件和动作指定的。默认情况下,Gem5包括各种可用的Ruby缓存一致性协议,但是,目前这些协议都不支持HTM。
HTM可以以多种方式实现,具有不同的权衡。为了使ARM的TME成为原型,我们选择了一种在微体系结构中实现ISA扩展的特殊方式。简而言之,它使用惰性版本管理和具有高速缓存线粒度的紧急冲突检测(Bobba等人,2007)。其他实现也是可能的,并且针对Gem5设计的选择不一定反映任何硅实现-现有的或未来的。
图3:橙色突出显示的框是微体系结构中已添加或修改以适应TME支持的组件。
有几种技术可用于寄存器检查点,包括影子寄存器堆和冻结物理寄存器。在ge5中,我们选择了一种没有开销的功能正确的检查点机制,即整个寄存器文件的零周期瞬时备份。这允许我们在核心模型之间共享公共检查点机制。
为了将HTM支持与特定于TME的功能分开,将用于创建和恢复检查点的通用接口添加到src/arch/目录。ISA可以根据其特定需求实现此接口,从而允许许多HTM功能被共享和重用。TME实现还必须能够回滚体系结构状态,并在事务失败或取消时丢弃推测性更新。这是通过重新调整ge5的异常机制来实现的。
为了跟踪事务的读/写集并缓冲推测性内存更新,我们利用高速缓存一致性协议。Gem5包含Ruby协议MESI_THERE_LEVEL。MESI指的是缓存线可能处于的状态:已修改、独占、共享或无效状态(Sorin,Hill,&;Wood,2011)。该协议利用由更大的统一包含式专用L2高速缓存馈送的专用L1数据和指令高速缓存。L2缓存由更大的包含式共享L3缓存和Coherence目录提供支持。
对MESI_Three_Level协议进行了增强,以支持TME。L1数据高速缓存用于缓冲与系统其余部分隔离的推测状态。因为L2缓存是包含的,所以它包含事务的读/写集中使用的相同行,但是保存它们的事务前的值。此配置的结果是事务的工作集必须仅驻留在L1数据缓存中。如果以事务方式读取或写入的行溢出,即从L1数据高速缓存逐出到L2高速缓存,则必须中止该事务,并且必须丢弃任何推测性写入的数据。
为了跟踪事务读取和写入状态,向每个L1数据高速缓存线的标签添加两个额外的‘位’-如果它在事务的读取集中,则添加1位;如果它在写入集中,则添加另一个位。然后,在从一个高速缓存线状态转换到另一个高速缓存线状态时使用这些位。要提交事务,需要清除这两个位。要中止事务,如果该行同时被修改,并且在事务的写入集中,该行将转换为无效;与提交类似,这两个位也都会被清除。我们假设可以自动清除这些位,以便对于外部观察者,要么提交(变为非推测性)所有事务状态,要么丢弃并回滚。这满足原子性的事务内存属性。
为了测试ge5中的新功能,我们概述了一个用C语言编写的简单程序,该程序使用TME事务并行更新直方图。此程序使用手动锁省略-锁用于保护共享数据结构,但在任何可能的情况下都会为了有利于事务而绕过(即避开)锁。如果事务不能取得进展,这可以满足后备路径的要求。
我们首先定义一个非常简单的自旋锁,它使用AArch64的弱内存模型。
#include<;stdatomic.h>; Tyfinf atom_int lock_t; 内联void lock_init(lock_t*lock){ Atom_init(lock,0); } 内联空锁获取(lock_t*lock){ WHILE(ATOM_EXCHAGE_EXPLICIT(LOCK,1,Memory_Order_Acquisition)) ;//旋转到收购为止 } 内联int lock_is_Acquired(lock_t*lock){ 返回ATOM_LOAD_EXPLICIT(LOCK,Memory_Order_Acquisition); } 内联void lock_release(lock_t*lock){ ATOM_STORE_EXPLICIT(LOCK,0,Memory_Order_Release); }。
接下来,我们编写一个函数来使用TME事务取消锁。如果成功解除锁定,LOCK_ACCENTER_ELIDED返回1,否则返回0。该函数启动一个新事务,并检查锁是否仍然空闲,从而将其添加到事务的读取集中。如果锁不是空闲的,事务将通过TCANCEL显式中止。在我们的示例中,作为参数传递的特定15位整数并不重要,但是,设置MSB可以确保事务可以重试。
#include<;arm_acle.h>; #定义TME_MAX_RETRIES 3 #定义TME_LOCK_IS_ACCENTED 65535 Int lock_Acquisition_elided(lock_t*lock){ Int num_retries=0; Uint64_t状态; 做{ Status=__tstart(); 如果(状态==0){ //检查是否获取了锁,并将其添加到我们的读集合中 IF(LOCK_IS_ACCENTED(LOCK)){ __t取消(Tme_Lock_Is_Acquired); __builtin_unreacable(); } 返回1; } ++重试次数; }WHILE((STATUS&;_TMFAILURE_RTRY)&;&;(Num_Rtries<;TME_MAX_RETRIES)); //事务失败次数过多 返回0; } Void lock_release_elid(){ __tCommit(); }。
然后利用这些自旋锁和事务例程来创建更新堆上的全局共享数组结构的函数工作。此函数可以从多个线程并行调用。
#include<;stdio.h>; #include<;stdlib.h>; #include";lock.h"; #定义ARRAYSIZE 512 #定义迭代次数10000 波动率长整数直方图[ARRAYSIZE]; Lock_t global_lock; Void*work(void*void_ptr){ //RNG种子使用线程id, //这将防止线程生成相同的数组索引。 Long int idx=(Long Int)void_ptr; UNSIGNED INT SEED P=(UNSIGNED INT)IDX; INT I,RC; Printf(";来自线程%ld\n";,idx的问候); For(i=0;i<;迭代;i++) { Int number 1=rand_r(&;Seed)%ArraySIZE; Rc=LOCK_ACCENTER_ELIDED(&;GLOBAL_LOCK); IF(rc==0)//解锁失败 LOCK_ACCENTER(&;GLOBAL_LOCK); //启动临界区 Long int temp=直方图[num1]; 温度+=1; 直方图[num1]=温度; //结束临界区 IF(RC==1) Lock_release_elid(); 其他 LOCK_RELEASE(&;GLOBAL_LOCK); } Printf(";来自线程%ld\n";,idx的再见); }
最后,我们使用一个派生和联接工作线程的主函数将所有这些放在一起。
#include<;assert.h>; #include<;pthread.h>; #include<;unistd.h>; int main(){ Long int I,Total,number of Processors; Pthread_t*个线程; INT RC; Number OfProcessors=sysconf(_SC_NPROCESSORS_ONLN); Printf(";TME具有%ld进程的平行直方图\n&34;,number OfProcessors); Lock_init(&;global_lock); //初始化数组 For(i=0;i<;ArraySIZE;I++) 直方图[i]=0; //派生工作 线程=(pthread_t*)malloc(sizeof(Pthread_T)*number OfProcessors); For(i=0;i<;number OfProcessors-1;i++){ Rc=pthread_create(&;threads[i],null,work,(void*)i); Assert(rc==0); } Work((void*)(number OfProcessors-1)); //等待工作线程 For(i=0;i<;number OfProcessors-1;I++){ Rc=pthread_join(threads[i],NULL); Assert(rc==0); } //校验数组内容 总数=0; FOR(i=0;i<;数组;i++) 总数+=直方图[i]; //免费资源 释放(线程); Printf(";合计为%lu\n预期合计为%lu\n";, 总计,迭代次数*number of fProcessors); 返回0; }。
从版本10开始,GCC支持TME-这包括ALE内部功能。要编译带有TME指令的源文件,必须使用AArch64编译器,并通过进行码标志启用该功能,例如,-march=ARMv8-a+tme。
具有两个处理器的TME并行直方图 来自线程%1的问候 来自线程0的问候 来自线程1的再见 来自线程0的再见 总数是20000美元 预计总数为20000 正在退出@Tick 718668000,因为正在退出上次活动的线程上下文。
为了验证是否有任何关键部分以事务方式执行,我们检查m5out/stats.txt,其中有几个与HTM相关的统计数据。
Htm 35 22.01%22.01%#system.ruby.l0_cntrl0.sequencer.htm_transaction_abort_cause::explicit事务中止的原因 Htm 38 23.90%45.91%#system.ruby.l0_cntrl0.sequencer.htm_transaction_abort_cause::transaction_size事务中止的原因 System.ruby.l0_cntrl0.sequencer.htm_transaction_abort_cause::memory_conflict 86 54.09%100.00%#HTM事务中止的原因 System.ruby.l0_cntrl0.equencer.htm。_TRANSACTION_ABORT_CAUSE::TOTAL 159#HTM事务中止的原因 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::samples 9927#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::mean 63.466103#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::gmean 56.438036#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::stdev 29.。029108#外部事务花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::32-47 4854 48.90%48.90%#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::48-63 2 0.02%48.92%#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::64-79 195 1.96%50.88%#所用周期数。在外部交易中 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::80-95 462746.61%97.49%#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::96-111 1881.89%99.39%#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::112-127 60 0.60%99.99%#外部事务中花费的周期数 System.ruby。.l0_cntrl0.sequencer.htm_transaction_cycles::128-143 1 0.01%100.00%#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_cycles::total 9927#外部事务中花费的周期数 System.ruby.l0_cntrl0.sequencer.htm_transaction_instructions::samples 9927#外部事务中花费的指令数 System.ruby.l0_cntrl0.sequencer.htm_transaction_instructions::mean 12#外部事务中花费的指令数 系统红宝石。L0_cntrl0.sequencer.htm_transaction_instructions::gmean 12.000000#外部事务中花费的指令数 System.ruby.l0_cntrl0.sequencer.htm_transaction_instructions::12-13 9927 100.00%100.00%#外部事务中花费的指令数 System.ruby.l0_cntrl0.sequencer.htm_transaction_instructions::total 9927#外部事务中花费的指令数。
这些是每个核心的统计信息,提供有关事务长度的信息(以周期数或指令数表示),以及中止事务的原因。当使用TCANCEL时,EXPLICIT会递增-在我们的示例代码中,当观察到全局锁在事务已经启动之后被获取时,就会发生这种情况。当另一个处理元素试图修改事务的读或写集中的高速缓存线时,就会发生MEMORY_CONFLICATION。TRANSACTION_SIZE指示事务溢出出一级数据缓存;由于这很难准确跟踪,因此
这些是采样直方图,其中包含以数量表示的事务大小。
.