Intel Transactional Synchronization Extension (TSX) 事务性同步扩展

这是一份Intel IDF2013上的讲座笔记。

TSX是新一代Haswell架构上,通过硬件支持的事务性内存(Transactional Memory)解决方案。

1.    动机

一句话概括Intel的事务性同步扩展(Transactional Synchronization Extension, TSX)的动机:粗粒度锁保证的事务性操作,在高并发下性能下降,作为细粒度锁方案的一种替代,TSX通过硬件辅助保证正确性,编程更友好。

2.    场景

TSX适用的场景:

一张有大量数据的表(原讲座中用银行账户记录做比),一种典型的事务性操作,是从其中一个条目a中减去一个数值x,加到条目b中:

[ a = a – x : b = b + x ]                                 (1)

         如果用一个粗粒度的锁保护整张表的操作,在并发时会碰到如下的问题,比如同时另一个线程对不同的条目c和d操作,原本不冲突的操作,因为粗粒度锁的存在,不得不串行执行。可以想见,高并发下粗粒度锁的方案性能严重下降。

         传统的优化手段是使用细粒度的锁,比如给表中的每一个条目单独加锁,那么上面存在的“假”的数据冲突就可以避免。但是细粒度锁极大增加的设计的复杂度,容易出现难以解决的bug。作为一个例子,考虑在(1)的操作同时,另一个线程进行如下操作:

[ b = b – x : a = a + x ]                                 (2)

         由于a/b由两个独立的锁保护,完成(1)或(2)的操作,需要获得两把锁,如果(1)(2)不能完整获得两把锁,而是(1)获得lock(a),(2)获得lock(b),即出现死锁。所以细粒度锁的加锁解锁方案需要仔细设计,避免死锁和其他很多问题。

         使用TSX的替代方案:逻辑上TSX是一把粗粒度的锁,将包含事务性的操作的critical section包起来;由硬件自动检测操作中的数据冲突,保证事务性操作的正确性,发掘操作间的并行性,实现上类似每个条目都有细粒度的锁,这被称作lock elision。

       TSX不适用的场景:

       从上面的例子可以看出,TSX主要解决的是粗粒度锁下的“假”数据冲突问题,如果原本不需要细粒度的锁,或者产生冲突的条目少,“真”冲突概率高,那么使用TSX的收益不大。TSX不是银弹。

Q: 我怎么知道什么时候该使用TSX?

A: 如果现在的程序没有性能问题,你可以去休息,喝杯咖啡;如果有我上面场景中的性能问题,你可以试试TSX,用起来也很方便。

3.    性能

典型应用场景下,相对粗粒度锁的方案,TSX的方案在高并发下的性能有明显提升,可以达到接近线性的可扩展性;相对细粒度锁的方案,TSX在高并发下的性能也有小的优势(TSX的开销可能比细粒度锁的开销小)。图不方便贴了。

相比比较火的无锁编程,TSX也有明显的优势。无锁编程不是真的没有锁,而是很强的依赖于原子操作,它的劣势是限制了数据结构(只能用队列等),难于设计和优化,高并发下也有问题。TSX下数据结构的选择更自由(因为使用的是和粗粒度锁一样的临界区的模型),同样的需求用无锁编程难以验证正确性。

4.    编程

TSX的模型类似传统的临界区。提供两种编程接口:HLE(Hardware Lock Elision)和RTM(Restricted Transactional Memory)。以如下的伪代码为例:

acquire_lock(mutex);

// critical section

release_lock(mutex)

传统的基于锁的方案大概是这样的:

        mov eax, 1

Try:   lockxchg mutex, eax

       cmp  eax, 0

       jzSuccess

Spin:  pause

       cmp  mutex, 1

       jz  Spin

       jmp  Try

; critical section …

Release:   mov  mutex, 0

HLE

使用一对compiler hints:xacquire /xrelease。

mov eax, 1

Try:   xacquirexchg mutex, eax

       cmp  eax, 0

       jzSuccess

Spin:  pause

       cmp  mutex, 1

       jz  Spin

       jmp  Try

; critical section …

Release:   xrelease  mutex, 0

提示:

(1)   两个关键词是hints,在不支持TSX的硬件上直接被忽略。

(2)   事务性操作失败(abort)的结果是重新执行传统的有锁代码(legacy code)。

RTM

RTM使用两条新的指令标识criticalsection:xbegin / xend。

RTM的模型更加灵活:

Retry: xbeginAbort

       cmp  mutex, 0

       jzSuccess

       xabort$0xff

Abort:

       …check %EAX

       …do retry policy

       …

       cmp  mutex, 0

       jnz  Release_lock

       xend

提示:

(1)   事务性操作失败(abort)的后续操作入口由xbegin指定。

(2)   xabort指令通过eax返回一个错误码,用于后续分原因处理。

软件环境

操作系统不需要改变。

主流编译器支持:ICC v13.0以上,GCC v4.8以上,Microsoft VS2012以上。

库:GLIBC的pthread rtm-2.17分支支持。

(我:找到网上有一个C版本的TSX使用例子,http://software.intel.com/en-us/blogs/2012/11/06/exploring-intel-transactional-synchronization-extensions-with-intel-software)

Q: pthread中怎么使用TSX?

A: 只需要动态链接这个版本的pthread库就可以(我:看来pthread使用了TSX重构了一些代码,而不包括TSX的高级封装)。

5.实现细节

支持嵌套。

临界区内,大部分事件不会导致abort,包括但不限于分支预测失败、缓存不命中、TLB不命中等。

对临界区内指令或者操作的数量和类型没有显式的限制(有隐性限制,下面讲)。

实现的方法

xacquire开辟一个缓存,存储当前状态(寄存器等),(Q.A.这个存储在哪与实现相关,可能是L1 cache)。所有后续的内存写操作被缓存下来,不会真正提交更新。使用L1 cache记录跟踪所有读写的物理地址(缓存中的数据被替换也不会阻止跟踪)。硬件检测是否存在读写冲突,使用现有的cache协议。所有的跟踪和检测以cache line为最小粒度。如果检测到读写冲突,触发abort,所有缓存下来的更新被放弃;如果没有冲突,提交更新,所有线程立即可见,这个过程不需要cpu核或者线程间的通信。

提示:避免cache false sharing,否则严重影响TSX的性能。

Q: TSX对临界区内代码数量有没有限制?

A: 没有显式的限制,但是比如L1cache的大小明显是一个隐性限制,如果存储操作太多以致无法在cache中全部跟踪,将导致abort。另外,TSX的使用如果导致L1 cache都不够用,我只能认为你的临界区太大了,不符合临界区设计的原则,应该去修改你的程序。

附:Intel TSX手册 - http://software.intel.com/sites/default/files/m/9/2/3/41604

你可能感兴趣的:(C/C++,编程工具)