彭茜珍,胡 莉
(湖北科技学院 学报编辑部,湖北 咸宁 437100)
并发程序设计中经常利用事务内存(Transactional Memory,TM)技术简化程序的编写。TM技术源于数据库社区最先提出和发展起来的概念[1],其主要思想是把一段任务代码定义为一个事务(Transaction )。借助事务的原子执行提交任务的所有结果到内存(当这个事务成功),或中止并取消任务所有的结果(当这个事务失败)。TM的关键是保证原子性(Atomicity),一致性(Consistency )和隔离性(Isolation )这些要素,这样利用事务内存技术,多线程设计人员可以消除现有的开发难度和容易犯错误的技术,使程序可以安全地并行执行。本文给出了一种基于Intel TSX指令集的硬件监控多个线程运行中出现的冲突内存访问的功能,实现一种TM技术的模式。
Intel TSX(Intel Transactional Synchronization Extensions)是X86指令集架构(ISA)的扩展,它提供了硬件事务内存支持,对粗细粒度线程进行锁定。通常在多核多线程CPU中,当多个线程对某一共享数据都需要访问时,需要做出某种仲裁。当一个线程访问该共享数据时,另一线程就无法访问,否则就会发生错误。一般的程序设计者,为了预防线程间的竞争,阻止错误发生,都使用粗粒度的锁,这导致该线程占用很多的共享数据,使其他线程都不能争抢。这样也使一些本不需锁定的共享数据也都被锁定了,其他线程无法利用,从而降低了多核CPU的多线程性能,增加了开发难度。TSX指令就是要让程序设计者能够更方便、更精准地做细粒度锁定,让这些共享数据更充分有效地运用。
2013年6月,Intel TSX指令首次在基于Haswell微架构的Intel微处理器中引入,随后经过Intel公司几年的不断的完善和更新,如今已成为Intel微处理器的标配指令集扩展[2]。
使用Intel TSX,程序设计者通过指定的代码临界区域(也称为事务区域)使整个临界区以事务方式执行。如果事务的运行成功地完成,这样在从其他逻辑处理器查看时,这个事务临界区内执行的所有内存访问都会立即发生。处理器仅在成功提交时才会对其他逻辑处理器可见的区域内执行架构更新,这个过程即为原子提交。
成功的事务执行确保了原子提交,因此CPU在没有显式同步的情况下就会乐观地执行临界区代码。如果这个特定的执行无需同步,则该执行就在没有任何跨线程串行化的情况下提交。如果CPU无法进行原子提交,则此次乐观执行失败。当发生这种情况时,CPU回滚该执行,这个过程即为事务中止。进行事务中止时,CPU放弃在该临界区中执行的所有更新,恢复CPU架构状态到未发生过乐观执行之前的情形,同时做非事务性地恢复执行。
Intel TSX提供了两个特殊软件接口,用于指定事务运行的代码临界区。硬件锁定省略(Hardware Lock Elision,HLE)接口指定的事务代码临界区用于传统的兼容指令集扩展。受限事务存储器(Restricted Transactional Memory,RTM)接口是一种新的指令集接口,它给程序员提供比HLE更有弹性的方式定义事务代码临界区。此外,Intel TSX还提供允许软件查询逻辑处理器是否处在由HLE或RTM标识的事务代码临界区中做事务执行。
为了支持硬件事务内存,并向程序设计者提供细粒度锁,Intel TSX引入了两类新的软件接口,硬件锁定省略和受限事务存储器,以及和它们相关联的指令[3,4]。
提供两个新的指令前缀XACQUIRE和XRELEASE。这两个前缀可以重用现有REPNE/REPE前缀(F2H/F3H)的操作码。在不支持TSX的处理器上,在XACQUIRE/XRELEASE有效的指令中忽略REPNE/REPE前缀,从而实现向后兼容性。
(1)XACQUIRE前缀,格式:XACQUIRE,它向处理器提供一个带有“XACQUIR开启”的指令的提示,用于在指令存储器操作数地址上启动锁省略。
(2)XRELEASE前缀,格式:XRELEASE,它向处理器提供一个带有“XRELEASE开启”的指令的提示,用于在指令存储器操作数地址上结束锁省略。
HLE允许通过省略对锁的写入来乐观地执行代码临界区,从而使锁看起来对其他线程是自由的。
提供了三条新指令:XBEGIN,XEND和XABORT。 XBEGIN和XEND指令标记事务临界代码区的开始和结束; XABORT指令显式地中止一个事务。事务失败会将处理器重定向到由XBEGIN指令指定的回退代码路径,并在EAX寄存器中返回中止状态。
(1)XBEGIN指令,格式:XBEGIN rel16/ rel32,它指定RTM区域的开始,指令操作数相对偏移量,以计算在RTM中止后执行恢复的回退指令地址的地址。如果逻辑处理器尚未处于事务执行中,则XBEGIN指令会使逻辑处理器转换为事务执行。这时将逻辑处理器转换为事务执行的XBEGIN指令称为最外层的XBEGIN指令。在RTM中止时,逻辑处理器丢弃在RTM执行期间执行的所有架构寄存器和更新的内存,并将架构状态恢复为与最外层的XBEGIN指令相对应的架构状态。中止后的回退地址是由最外层XBEGIN指令计算的。
(2)XEND指令,格式:XEND,它指定RTM区域的结尾。如果这对应于最外层范围(即,包括此XEND指令,XBEGIN指令的数量与XEND指令的数量相同),则逻辑处理器将尝试以原子方式提交逻辑处理器状态。如果提交失败,逻辑处理器将回滚在RTM执行期间执行的所有架构寄存器和内存更新。逻辑处理器将从最外面的XBEGIN指令计算的回退地址处恢复执行。
(3)XABORT指令,格式:XABORT imm8,它强制RTM中止。在RTM中止之后,逻辑处理器通过最外面的XBEGIN指令计算的回退地址处恢复执行,同时更新EAX寄存器以反映导致中止的XABORT指令,并且将在EAX的位31∶24中提供imm8参数。
RTM可以对HLE进行替代实现,它使程序设计者可以灵活地指定在无法成功执行事务时执行的回退代码路径。
提供一条新的XTEST指令,用于确定处理器是否正处在一个事务区域中。
XTEST指令,格式:XTEST,它查询事务执行状态,并通过ZF标志中反映查询结果。如果指令在事务执行的RTM区域或事务执行的HLE区域内执行,则清除ZF标志,否则设置。
借助于这套TSX指令的方案,在高度并发操作下,应用性能提升明显,能够达到接近线性的扩展;相对于其他细粒度锁的方案,TSX在高度并发操作下的性能也很有优势(通常TSX的开销比其他细粒度锁的开销小)。与无锁编程相比,TSX也有很明显的优势[4]。
支持Intel TSX扩展的Intel CPU架构提供了一个无序、单一版本、强隔离的可嵌套TM。这个TM以一个Cache行为粒度,跟踪read-set和 write-set。
在Intel CPU架构中,每个核都有L1和L2两级Cache(由此核线程共享),以及一个统一的L3(3级Cache)由CPU内所有核共享的。TM的一致性的变化仅限制在L1D和L2中,并且严格限于核内。同时扩展了CPU架构中已有的Cache行为和MESIF一致性协议 ,以其增加支持TM的语义。
基于上述结论,可以探究支持Intel TSX扩展的Intel CPU架构的事务内存的关键原理,其中最重要的关键细节包括TM单一版本管理、冲突处理以及提交/中止操作三个方面。
这种架构的TM最终采用是延迟更新系统,使用CPU中每个核的cache作为事务数据和寄存器检查点。具体的做法是,L1D和L2的每个cache行中的标记增加一些比特位,记载该行是否属于可以在CPU核上执行的线程的read-set(RS)或write-set(WS)。事务中的一个存储操作只是写cache行,而共享的L3保存原始数据(对于事务中的第一次存储,还需要把L3更新为Modified )。这与Intel自从推出Nehalem以来一直沿用至今的共享和包容性的L3缓存是一致的。为了避免数据出错,任何事务性数据(即标记有RS或WS的缓存行)必须保留在L1D或L2中,而不能被替换到L3或内存。属于体系结构的寄存器应被设置成检查点,并存储在CPU芯片中,并且事务可以随机地读取和写入寄存器。
因此,这种架构的事务内存是将事务的write-set 和 read-set维护在L1D中的。L2是非事务性的,所以,当一个write-set的cache行被替换出时,事务就会中止。在一些情形下, read-set的cache行可以被安全地替换出L1D,同时利用另一个硬件机制进行跟踪。在发生中止时,所有标记为write-set的cache行都会从LD1中清除,而提交使得标记为write-set的cache行原子可见。这样,得益于TSX的吞吐量,可以使CPU的最小锁延迟足够小。
这种架构的冲突检测使用现有的MESIF一致性协议处理,它通过对内存的排序缓冲、L1D和L2高速缓存做一些调整和增强来实现。发生冲突只能有三种方式,它们都能很轻易地被检测到。
第一种情况是,一个标记为RS的cache行被另一个核写。借助于MESIF协议,写一个共享cache行,就会无效任何其他私有cache的副本。另一个核会发出一个read-for-ownership请求给共享的L3,这导致检查嗅探过滤器,同时发出一个无效命令给具有该cache行副本的任何cache。如果L1D和L2接收到对一个标记为RS的cache行无效的命令,这表明有另一个线程试图在写,这时发生了冲突。
第二种情况是,另有一个核访问标记为WS的cache行。为使事务能在第一时间写cache行,其他核不能够缓存该副本,这是Modified状态一致性的标准语义。然而,包容性的L3会保留事务之前的已过时原始版本。所以,当有另一个核企图访问标记为WS的cache行时,它就会在其本地的L1D和L2中产生缺失,进而会探测L3。之后,L3将访问嗅探过滤器,接着发出read(或read-for-ownership)请求给带有标记为WS的该cache行的核。因此,如果L1D和L2中标记为WS的一个cache行收到了任何读请求(或无效请求),这时就发生了冲突。
第三种情况是,在同一个CPU内核中,共享该内核的L1D和L2的另一个线程会对事务进行干扰。此时,另一个线程将命中共享的L1D和L2,但不对L3触发任何一致性通信。因此,可通过检查cache行是否被另一个线程做了WS标记,放置在加载流水线中处理;还可检查cache行是否在被另一个线程做了RS或WS标记,使其在存储流水线中处理。
最后的注意是,当标记为RS或WS的cache行被替换到L3或内存中,应立即中止事务。这样做会使得有关问题容易处理。Intel修改了L2行的替换和驱逐策略,以防止替换出事务性数据。将L2用于事务还有一个优点是,与L1D相比,L2替换策略能够容忍更多延迟,这是出现在任何L1D缺失的关键路径上。
在支持的Intel TSX扩展的CPU架构中,提交事务是一个简单的问题。L1D和L2的控制器,确保在WS中的任何Cache行处于Modified 状态,并将WS位清0。并且任何带有RS的Cache行必须是在Shared状态,RS位同样被清零。旧的寄存器文件检查点应删除,把寄存器文件的现有内容变为体系结构状态。
基于一致性的冲突检测方案可以在冲突发生时尽早识别到,而不必等到提交时。冲突被发现的早,就会立即终止该事务。
中止事务,比提交事务更简单。Cache控制器确定所有WS的缓存行为Invalid状态,并将WS位清0。控制器还必须将RS位清0,但不需无效Cache行。最后,从先前的检查点恢复体系结构寄存器文件。
Intel TSX指令基于处理器硬件设施,使用之前需要做3项检测[2]:HLE 支持的检测、RTM 支持的检测、XTEST指令的检测,检测通过后方可使用,否则会产生#UD 异常。另外,还需要注意的是在某些CPU实现中,一些特别的指令可能总是导致事务中止,对于这些指令不要在代码临界事务区内使用。下面给出Intel TSX指令应用举例及指导[6]。
(一)通常加锁的代码临界区的流程是这样的:
acq_lock(lock) ;请求锁并且成功
; 临界区代码
re_lock(lock) ;释放请求的锁
(二)一般的汇编实现流程如下:
mov eax, 1
ReTry: lock xchg lock, eax
cmp eax, 0
jz Success ;成功获取锁转到临界区代码
Spin: pause ;不成功锁自旋
cmp lock, 1
jz Spin
jmp ReTry ;重新获取锁
Success:
… ;这里是临界区代码
mov lock, 0 ;释放锁
(三)使用Intel TSX指令HLE接口的汇编流程如下:
mov eax, 1
ReTry: xacquire xchg lock, eax
cmp eax, 0
jz Success
Spin: pause
cmp lock, 1
jz Spin
jmp ReTry
Success:
… ;这里是临界区代码
xrelease mov lock, 0 ;释放锁提交HLE执行
(四)使用Intel TSX指令RTM接口的汇编流程如下:
ReTry: xbegin Abort ;开始一个RTM
cmp lock, 0 jz Success
xabort $0xff
Abort:
… ;检测EAX
… ;根据检测结果或回滚重新开始一个RTM;或重新请求lock
Success:
… ;这里是临界区代码
cmp lock, 0
jnz Release_lock
jmp Endd
Release_lock:
mov lock, 0 ;释放锁
Endd:xend ;提交RTM执行
(五)很多C++编译器软件都对Intel TSX指令进行包装,方便设计者使用,下面是GCC编译器对RTM的包装及其用法[7]:
#include
if ((status = _xbegin ()) == _XBEGIN_STARTED) {
... transaction code...
_xend ();
} else {
... non transactional fallback path...
}
注意:对在事务代码临界区内访问的高速缓存行的冲突请求可能会阻止事务区成功执行。此外,其他一致性流量有时可能会显示为冲突请求并可能导致中止。虽然这些非真正冲突可能会发生,只是它们并不常见,设计者还需留意。
多线程应用程序设计越来越多地利用CPU多核来提高性能。但是,编写多线程应用程序会要求开发者实现多个线程之间的数据共享。访问共享数据经常使用同步机制—锁。这些同步机制多半采用串行化进行共享数据的操作,以确保多个线程安全地使用共享数据,一般使用锁保护的临界区实现更新共享数据操作。串行化的缺点是大量使用锁机制保护不同的共享数据,限制了并发性,增加了开发难度并且容易出错。Intel TSX 指令的突出特点是很大程度降低这类开发的难度,为多核程序设计者带来了一项新的多线程开发手段。