X86原子RMW指令是否等待免费

时间:2020-05-12 05:31:49

标签: concurrency x86 atomic lockless wait-free

在x86上,原子RMW指令(如lock add dword [rdi], 1)是通过在现代CPU上使用缓存锁定来实现的。因此,在指令期间,缓存线被锁定。这是通过在读取值时获取行EXCLUSIVE / MODIFIED状态来完成的,并且在指令完成之前,CPU不会响应来自其他CPU的MESI请求。

并发进度条件有两种,阻塞和非阻塞。原子RMW指令是非阻塞的。 CPU硬件在保持高速缓存锁定时将永远不会休眠或执行其他操作(中断发生在原子RMW之前或之后,而不是在此期间),释放高速缓存行之前的步数上限是有限的(且很小)

在理论计算机科学中,

Non blocking algorithms可以分为3种形式:

  1. 免费等待:所有线程将在有限的步骤中取得进展。

  2. 无锁:至少一个线程将在有限的步骤中取得进展

  3. 畅通无阻:如果没有争用,线程将在有限的步骤中取得进展

x86提供什么样的保证?

我猜它至少是无锁的;如果有争用,至少有一个CPU会取得进展。

但是x86是否可以免费等待原子指令?是要保证每个CPU都能在有限的步骤中取得进展,还是一个或多个CPU处于饥饿状态并可能无限期地延迟?

那么,当多个内核在同一条缓存行上执行原子操作时会发生什么?

2 个答案:

答案 0 :(得分:2)

考虑一个更一般的问题:如果有多个活动的硬件线程,则x86是否确保每个线程都向前发展,而不管其他线程做什么?您提出的问题似乎专门针对每个线程同时向重叠的内存位置执行原子指令的情况。如果答案是肯定的,则x86可描述为“无等待”。 (该术语通常仅用于描述线程同步算法,但是无论如何。)

我认为从体系结构或其实现的角度定义“前进”是很重要的。我不喜欢在定义中使用术语“步骤”,因为不清楚什么是步骤,什么不是步骤。取而代之的是,我将使用以下定义:一个活动的硬件线程在完成程序顺序中的下一条动态指令时可以通过退出它或在出现错误情况时切换到异常处理程序来向前推进。如果每个活动的硬件线程都可以在有限的时间内前进,而与其他线程的工作无关,并且与每个线程在执行什么指令无关,只要它们不会导致线程变为非活动状态,则x86处于等待状态-自由。 (请注意,中断处理程序不是在硬件线程上执行的程序的一部分,因此处理中断并不意味着该线程正在向前发展。)

是否确保每个CPU都能在有限的步骤中取得进展 还是一个或多个CPU饿了,或者 可能无限期地延迟?

您可能在这里认为,如果有两个内核不断尝试获取对同一位置的原子RMW访问,它们中的一个是否将始终成功而另一个将始终失败,从而在尝试执行相同的原子指令而不进行操作时陷入困境任何进展,因为它是程序顺序中的下一条指令。

这实际上是计算机体系结构中的传统问题。我想考虑一个更笼统的问题的原因是,除了获取锁之外,多个硬件线程或代理之间还有很多可能的争用点。考虑一下你说的话:

CPU硬件在保持运行状态时将永远不会休眠或执行其他操作 缓存锁定(中断发生在原子RMW之前或之后,而不是 期间),其数量的上限是有限的(且很小) 释放缓存行之前的步骤。
...
我猜它至少是无锁的。如果有争用,至少有一个CPU会取得进展。

Intel和AMD从未声明“在释放缓存行之前,步骤数有一个有限的上限”。这种推理几乎可以应用于指令执行的任何阶段。如果在专用缓存中丢失了获取指令,在获取指令的步骤数上是否有一个有限的上限?从共享缓存中读取值的步骤数是否有上限?使用超线程,争用的可能性几乎存在于执行任何类型指令的每个阶段。您可以对每个人问相同的问题。原子访问竞争并不特殊。有人可能会问其他问题,例如内核是否有可能随意进入睡眠状态而永不唤醒。

从根本上讲,如果没有通过设计确保在体系结构级别上拥有多个核心,那么只要每个核心处于活动状态(根据上述定义)就始终能够不断取得进步是没有意义的。否则,无法充分利用该实现。每个实用的ISA都必须提供最低限度的前进进度保证,即任何操作都需要有限的时间才能完成,并且在全局(或多代理)操作顺序之前必须进行有限数量的其他操作。某些ISA(例如RISC-V)确实明确指出了这一点。

在许多示例中,英特尔在SDM手册和许多其他文档中都明确指出,共享结构的设计可以保证公平性,这比最小的前进进度要强。 (由于性能或其他原因,这可能并不总是准确的,因为某些类型的请求可能始终具有更高或最高的优先级。也许最好说通常可以保证公平,总体上可以保证向前的进展,或者这些示例包括以下内容(从我的头开始):

  • 在Nehalem之前的多核处理器和Atom品牌的多核处理器上,L2超队列(包括L2控制器)被设计为(通常)公平并保证与其交互的所有代理的进度。
  • 前端总线(在具有FSB的系统上)和APIC总线(在具有单独的APIC总线的系统上)都被设计为公平的。
  • 同一内核上的硬件线程之间的大多数仲裁点都是公平的。在具有统一RS的微体系结构上的uop调度程序或在具有分布式RS的微体系结构上的uop调度程序是一个例外,它们使用优先准备的伪FIFO算法。
  • 在使用交叉开关互连的处理器上,在L3全局队列中保证公平。
  • 在具有环形互连的处理器上,在某些环形停靠点处保证公平,而在其他环形停靠点处仅保证前进。

因此,如果两个内核尝试获取对同一位置的原子RMW访问,则可以保证原子指令通过每个内核的管道和内存层次结构进行访问,并且每个内核的读取锁定请求最终将变为服务。因此,是的,根据上面的定义,x86无需等待。但是,值得注意的是,大多数或所有英特尔处理器都很少出现会导致全部或部分处理器无限期挂起的错误。

一个有趣的考虑因素是,是否可以确保不会由于连续处理中断而无限期地阻止内核的进程。我认为这主要取决于中断处理程序的设计,因此系统软件必须对此进行保证。

答案 1 :(得分:-1)

当多个线程碰巧锁定同一条缓存行时,它们的执行将被序列化。由于write contention,这称为false sharing

single-writer principle源于此。与读取相反,写入不能同时执行。

原子读取-修改-写入指令本身的执行时间是固定的,并且不取决于竞争高速缓存行的线程数。因此,在x86上,它们是wait-free population-oblivious

锁定竞争的缓存行所需时间的上限与缓存行所经历的竞争程度成正比。

来自Intel Community

在某些体系结构中,未选择首先执行的操作将被停止(然后由硬件重试,直到成功),而在其他体系结构中,它们将“失败”(基于软件的重试)。例如,在英特尔处理器中,如果目标内存位置繁忙,则硬件将重试锁定的ADD指令,同时必须检查锁定的“比较和交换”操作是否成功(因此,软件必须注意失败,然后重试操作。

由于将连续重试高速缓存行锁定,因此最终所有原子读取-修改-写入操作都将成功( operation 是指令加上硬件执行的重试以锁定高速缓存行)。
因此yes,保证每个CPU都能在有限的步骤中取得进展,而在x86上,整体上的原子读取,修改,写入操作都是wait-free bounded

按照相同的逻辑,x86存储区 operation 是无等待的有界,x86存储区 instruction 是无需等待的可填充的,而x86负载始终是等待-免费人口无视。

尽管像suggests的某人一样,ucode错误可能导致锁永远存在,但是在描述算法的风格时,我们不会考虑外部因素,而只会考虑逻辑本身。


获取缓存行锁不公平。

选择一个线程来获取锁的概率与close与释放锁的线程的比例成正比。因此,同一内核上的线程比共享L2高速缓存的线程比共享L3高速缓存的线程更有可能获得锁定。然后,较短的QPI / UPI / NUMA节点路径上的线程比其他线程具有优势,依此类推。

这同样适用于软件锁(自旋锁),因为发布发行存储时,它以相同的方式传播。


我在Intel Q4'17台式机CPU上运行了一个基准测试,证实了上述所有条件。
在同一存储位置上连续lock xadd切换时...

  • 在10秒内,在不同内核上运行的5个线程中,最快的线程lock xadd的运行速度是最慢的线程的2.5倍,而在不同的双向超线程上运行的10个线程中,运行速度的3倍更多
  • 3亿次,lock xadd的平均数量越来越小,花费的时间也越来越多,在不同内核上运行的5个线程的运行时间高达1.1ms,在不同双向运行的10个线程的运行时间达到193ms超线程

不同过程运行之间的差异很大。

相关问题