今天来看一个新概念,事务内存(Transactional Memory)。事务内存这个概念不算新,只不过ARM在最近几年才开始支持,其实现特性包含在事务内存扩展(Transactional Memory Extension,TME)内。
看过前面文章的朋友应该对同步不陌生。目前ARM的主流高性能处理器是单核单线程,没有实现同时多线程(Simultaneous Multi-Threading),下文讨论中不区分处理器核和线程。在多核系统中,共享资源需要被保护起来,在某一个时刻该资源只能被一个处理器核访问。多核之间需要有一些机制协调对共享资源的同时访问请求,这就是同步。
同步机制有锁(Lock),原子操作(Atomic Operation),等等方式。事务内存是一种较为宽松的同步机制。
解决同步问题最常用的是锁,即对临界区加上锁,只有获得锁的处理器才能访问临界区。常用的有自旋锁(Spinlock),互斥锁(Mutex)。但是这些锁机制会产生过度保护的问题,造成处理器停顿,进而影响整个系统的性能。以自旋锁为例,没有获得锁的处理器会等待一段时间,然后去申请锁,如果没有得到锁,那就重复上述过程。所谓的自旋,就是原地打转,这期间处理器也不会进行其它的操作,白白浪费了资源。使用锁也会引入一些其它的问题,比如优先级反转(Priority inversion)、死锁(Dead lock)、护航(Convoying)等。
在一些复杂系统中,锁的实现可以分为两个等级,粗粒度锁(Coarse-grained lock)和细粒度锁(Fine-grained lock)。粗粒度锁也称为全局锁,通过锁定对整个系统的访问来保持特定线程中的原子性。锁定访问可确保系统中没有来自任何其他观察者的干扰,这是因为试图访问任何内存空间的任何其它代码都会暂停,直到全局锁被移除。无疑,粗粒度锁对系统性能的影响很大。
在实现细粒度锁定时,将分析全局锁的使用情况,并根据需要对其进行优化。这意味着对锁进行本地化,以确保它们只在正在使用的地址上工作。因此,其他线程在操作中不会停止太多。细粒度锁可以部分缓解系统性能影响。
正是因为锁机制带来的种种问题,后来又产生了无锁(Lockless,有的地方也称为Lock-free)操作的概念。硬件提供原子性的读-改-写(read-modify-write)原语,程序通过调用这些原语完成对临界资源的访问。前面讲同步原语的文章中提到过。这种方法规避了使用锁时出现的上述问题并可以提高处理器效率,但是面临着原子操作本身功能局限性和组合性不佳的问题。无锁编程的算法设计通常很难。
事务内存来自于数据库管理系统中的事务概念。在数据库管理系统中,事务必须满足ACID性质,即原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability)。原子性指的是事务中的操作要么全部执行,要么一个都不执行;一致性指的是任何时刻,数据库必须处于一致性状态,即必须满足某些预先设定的条件;隔离性是指一个事务不能看见其他未提交事务所涉及到的内部对象的状态;而持久性则是指一个已提交的事务对数据库系统的改变必须是永久的,即使发生掉电,系统崩溃也不受影响。
事务内存是一种编程模型,它允许程序员将一段代码定义为一个事务,该事务要么全部执行,要么什么都不执行。事务内存与易失的内存打交道,所以持久性并不是重点。
事务内存有两种实现方式,基于软件的软件事务内存(Software Transactional Memory,STM)和基于硬件的硬件事务内存(Hardware Transactional Memory,HTM)。典型的STM是针对数据结构的,事务执行过程中在数据节点的副本上进行更新操作,事务提交时将指向数据节点的指针改为指向当前更新过的副本节点。在HTM中,事务执行过程中的写操作都被暂时缓存在专用的缓冲区(或专用的cache),如果事务成功提交,这些值被写入内存,如果事务回滚,这些值将被丢弃。事务的尺寸受限于专用缓冲区的容量,一旦超过则HTM性能急剧下降。
ARM功能扩展中支持的就是HTM。事务内存扩展(TME)向PE的执行添加了一个新状态,即事务状态(Transactional State)。事务定义为PE处于事务状态时执行的所有指令。换句话说,在事务外部执行的任何指令都被定义为在非事务状态下执行。TME向指令集中添加了新的指令:
- TSTART:事务开始指令
- TCOMMIT:事务提交指令
- TCANCEL:事务取消指令
- TTEST:事务状态和嵌套等级测试指令
先来看TSTART指令,其编码格式和语法格式如下,<Xt>是64-bit的通用寄存器:
TSTART <Xt>
TSTART指令在非事务状态下执行,然后PE进入事务状态。Xt寄存器由成功或失败值填充,这取决于事务是否已成功启动。如果TSTART成功,则寄存器值为0。如果事务失败,则将失败原因的编码写进目标寄存器,编码如下图所示:
- INT表示在事务中发生中断,但未执行中断。INT位用于提供信息,不是指明特定的故障原因。
- DBG表示异步外部调试异常导致事务失败。
- NEST表示嵌套深度超过设置深度导致事务失败。
- SIZE表示内存缓冲(保存上下文)大小超过了HTM中设置的限制。
- ERR表示在事务中架构执行导致事务失败。
- MEM表示HTM在事务中检测到内存冲突
- CNCL表示TCANCLE指令执行。
- RTRY表示事务可以重启启动
- IMP表示实现问题可能会导致事务失败。IMP还显示不属于上述任何类别的任何故障模式。RTRY值取决于实现和故障原因。
- REASON表示事务取消的原因
再来看一下TCOMMIT指令,该指令用于终止事务,将当前事务期间的所有系统上下文修改(包括任何嵌套事务期间的任何系统上下文修改)提交到当前架构状态。TCOMMIT指令还将PE状态从事务状态更改为非事务状态。TCOMMIT指令的编码格式如下图。
TCANCLE指令用于取消事务,取消所有上下文状态修改。指令中的立即数值在返回的目标寄存器中设置故障原因代码。TCANCLE指令的编码格式和语法格式如下:
TCANCEL #<imm>
TTEST指令根据事务状态以及是否存在任何级别的事务嵌套返回当前执行的状态。如果在非事务状态下执行,则该指令返回0;如果在外部事务中执行,则返回1。TTEST指令的编码格式和语法格式如下:
TTEST <Xt>
以下代码提供了一个简单的函数,以显示如何在简单事务中使用这些指令:
上面代码中,首先执行TSTART启动事务,把启动结果存入X16寄存器中;如果TSTART成功,执行下面的两条加载指令,然后执行TCOMMIT提交事务;最后,执行RET指令返回。
事务内存执行中不允许异常等级的任何更改或PE上的任何中断。在事务期间的中断都被挂起,直到事务完成或失败才会被响应。
但是考虑到一些系统对中断服务程序的延迟有要求,这时,具体设计实现可以允许中断打断事务。如果中断导致了事务失败,则返回的状态寄存器将IMP位设置为显示失败。如果实现允许中断打断事务,整个过程:
- 处理器在非事务状态下执行,中断被屏蔽。
- 处理器进入事务状态。
- 在事务状态下,中断不被屏蔽。
- 中断产生,实现允许中断使事务失败。
- PE然后使事务失败,并将上下文恢复到事务状态之前。在这种情况下,中断被屏蔽。这意味着在当前异常级别不会执行挂起的中断。
在满足上述所有条件的情况下,PE必须在事务状态退出时设置状态寄存器。为了允许PE响应异常,IMP位和INT位都被设置为允许回退代码信息取消屏蔽中断。不允许重试,因为这可能会导致活锁。
今天就到这里吧,是不是没用的知识又增加了一些呢?
作者: 老秦谈芯
文章来源:老秦谈芯
推荐阅读
更多IC设计技术干货请关注IC设计技术专栏。
迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。