TSX是新一代Haswell架构上,通过硬件支持的事务性内存(Transactional Memory)解决方案。
一句话概括Intel的事务性同步扩展(Transactional Synchronization Extension, TSX)的动机:粗粒度锁保证的事务性操作,在高并发下性能下降,作为细粒度锁方案的一种替代,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不是银弹。
Q: 我怎么知道什么时候该使用TSX?
A: 如果现在的程序没有性能问题,你可以去休息,喝杯咖啡;如果有我上面场景中的性能问题,你可以试试TSX,用起来也很方便。
典型应用场景下,相对粗粒度锁的方案,TSX的方案在高并发下的性能有明显提升,可以达到接近线性的可扩展性;相对细粒度锁的方案,TSX在高并发下的性能也有小的优势(TSX的开销可能比细粒度锁的开销小)。图不方便贴了。
相比比较火的无锁编程,TSX也有明显的优势。无锁编程不是真的没有锁,而是很强的依赖于原子操作,它的劣势是限制了数据结构(只能用队列等),难于设计和优化,高并发下也有问题。TSX下数据结构的选择更自由(因为使用的是和粗粒度锁一样的临界区的模型),同样的需求用无锁编程难以验证正确性。
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
使用一对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使用两条新的指令标识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的高级封装)。
支持嵌套。
临界区内,大部分事件不会导致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