【问题标题】:How is atomicity implemented by the CPU?CPU如何实现原子性?
【发布时间】:2014-08-17 14:25:32
【问题描述】:

我被告知/在线阅读了缓存一致性协议 MESI/MESIF:

http://en.wikipedia.org/wiki/MESI_protocol

还强制执行原子性——例如对于锁。但是,这对我来说真的没有意义,原因如下:

1) MESI 管理所有指令的缓存访问。如果 MESI 也强制执行原子性,我们如何获得竞争条件?肯定所有指令都是原子的,我们永远不会得到竞争条件吗?

2) 如果 MESI 保证原子性,LOCK 前缀的意义何在?

3) 如果原子指令使用与所有其他 x86 指令相同的缓存一致性模型实现,为什么人们会说原子指令会带来开销?

一般来说,有人可以解释一下 CPU 是如何在低级别实现锁的吗?

【问题讨论】:

  • 你不是刚刚ask this same question吗?
  • @KerrekSB 不完全是-我在回答后编辑了该问题以完善我的问题。现在我把它们都收集起来了——我创建了这个更集中的新问题。
  • 请不要那样做。如果您的观点基本相同,但您只是未能正确表达,请修复现有问题。您可以使用赏金来吸引未来的关注。
  • 您似乎混淆了可见性保证(没有线程看到旧内存值,缓存一致性协议的责任)与确保 am 指令以原子方式执行。 add [eax], 5 在现代 cpu 中不是作为一条指令执行,而是分成几个较小的微操作
  • 呸,这当然是可能的。因此,如果我调用没有 LOCK 前缀的“原子”x86 指令,这是否是原子指令,因为 CPU 具有确保指令在一个 CPU 周期内完成的特殊电路?或者这样的指令是否仍然需要原子性的缓存一致性策略?如果是前者我理解,如果是后者我很困惑。

标签: multithreading x86 cpu atomic cpu-architecture


【解决方案1】:

LOCK 前缀有一个目的,即对该地址进行锁定,然后指示 MESI 在随后的所有其他处理器上刷新该缓存行,以便所有其他处理器(或硬件设备!)读取或写入该地址块直到锁被释放(它在指令的末尾)。

LOCK 前缀很慢(几百个周期),因为它必须在持续时间内同步总线,并且总线速度和延迟远低于 CPU 速度。

LOCK指令的一般操作

1. validate
2. establish address lock on cache line
3. wait for all processors to flush (MESI kicks in here)
4. perform operation within cache line
5. flush cache line to RAM (which releases the lock)

免责声明:其中大部分来自 Pentium F00F 错误的文档(其中验证部分在建立锁定后错误地完成),因此可能已过时。

【讨论】:

  • 谢谢。你知道1和3的答案吗?
  • 3 已经回答。 1 不能通过任何数量的硬件来解决,因为它受图灵不完备定理的约束。
  • 您似乎将答案集中在 LOCK 前缀上。我的问题的重点是我已经阅读了 LOCK 前缀不是必需的,因为 MESI 将保证原子性。因此,对于第三季度,我不是指有关 LOCK 前缀的任何开销。如果非原子指令和原子指令都使用MESI协议处理,为什么原子指令会更昂贵?
  • 看不出Q1跟图灵有什么关系?如果 MESI 保证原子性并且所有指令都“通过”MESI,那么如果所有指令都是原子的,我们怎么能获得竞争条件?
  • “所有指令都是原子的”即使使用 MESI 也不是跨多个 CPU 的。未锁定的 inc 指令可以在读取和写入之间拆分。
【解决方案2】:

正如@voo 所说,您将连贯性与原子性混淆了。

缓存一致性涵盖许多场景,但基本示例是当 2 个不同的代理(多核芯片上的内核、多插槽芯片上的处理器等)访问同一行时,它们可能都将其缓存在本地. MESI 保证当其中一个写入新值时,所有其他陈旧副本首先失效,以防止使用旧值。作为副产品,这实际上保证了在高速缓存行粒度上对内存的单个读取或写入访问的原子性,这是 x86(以及许多其他架构)上 CPU 章程的一部分.它的作用不止于此 - 它是 CPU 为您提供的内存排序和一致性保证的关键部分。

然而,它没有提供任何更大规模的原子性,这对于处理线程安全和临界区等概念至关重要。您所指的锁定操作是读-修改-写流程,默认情况下不保证是原子的(至少在普通 CPU 上不保证),因为它由 2 个不同的内存访问组成。如果没有锁定,CPU 可能会在其间收到一个窥探,并且必须根据 MESI 协议做出响应。以下场景是完全合法的,例如:

  core 0       |      core 1
---------------------------------
y = read [x]   |
increment y    |    store [x] <- z 
               |
store [x] <- y |

意味着您在核心 0 上的内存增量操作未按预期工作。如果 [x] 持有一个互斥体,例如,您可能认为它是免费的并且您设法抓住了它,而核心 1 已经拿走了它。

锁定内核 0 上的读-修改-写操作(并且 x86 提供了许多可能的选项,锁定的 add/inc,锁定的比较交换等),将停止其他内核直到操作完成,所以它本质上增强了内核间协议以允许拒绝窥探。

应该注意的是,一个简单的 MESI 协议,如果使用正确的替代保证(如栅栏),可以提供无锁方法来执行原子操作。

【讨论】:

  • 只是强调一下,我相信 compare-exchange 也需要 LOCK 前缀(因为它唯一的 XCHG 具有自动锁定功能)。假设我是正确的,那么没有 LOCK 前缀的 CMPXCHG 的目的是什么,如果它不是原子的?
  • @user997112 是的,“锁定”也是为了申请那个,我会编辑。
【解决方案3】:

我认为关键在于,虽然缓存涉及普通内存操作,但对于原子操作,它需要做的更多,而不是普通的操作。


稍后添加...

对于普通操作:

  • 写入内存时,您的典型核心/cpu 将保持写入 队列,以便一旦发送写入,核心/cpu 继续处理指令,而其他一些机制处理 清空待写的队列——与 根据需要缓存。在某些处理器上,未决的写入不需要 按照放入队列的顺序写入。

  • 从内存读取时,如果需要的值不是立即 可用时,核心/cpu 可以继续处理指令,而 其他一些机械师执行所需的读取——与 根据需要缓存。

所有这些都是为了让核心/cpu 继续运行,尽可能地与通过缓存层访问真实内存的真正可怕的业务分离,这一切都非常 .

现在,对于您的原子操作,核心/cpu 的状态必须与缓存/内存的状态同步。

因此,对于“释放”存储:(a) 写入队列中的所有内容都必须完成,在 (b) “释放”写入本身完成之前,在 (c) 正常处理可以继续之前。因此,可能不得不放弃异步写入缓存/内存的所有好处,直到原子写入完成。同样,对于“获取”加载:必须延迟“获取”读取之后的任何读取。

事实上,x86 非常“表现良好”。它不会对写入进行重新排序,因此“发布”存储不需要任何额外的工作来确保它位于任何早期存储之后。在读取方面,它也不需要为“获取”做任何特别的事情。如果两个或更多核心/cpu 正在读取和写入同一块内存,那么缓存行的无效和重新加载将会更多,随之而来的是开销。在进行“顺序一致”存储时,必须紧跟明确的mfence 操作,这将停止 CPU/核心,直到所有写入都从写入队列中刷新。确实,“顺序一致”更容易考虑……但是对于共享数据的访问受锁保护的代码,“获取”/“释放”就足够了。

对于您的原子“读取-修改-写入”及其条件版本,与缓存/内存的交互更加强大。执行操作的 cpu/core 不仅要与缓存/内存的状态同步,还必须安排其他访问原子操作对象的 cpu/core 停止,直到它完成并被写掉(committed缓存/内存)。其影响将取决于当时是否与其他 cpu(s)/core(s) 存在任何实际争用。

【讨论】:

  • 我只是想弄清楚是原子指令执行这个词,还是 MESI 协议?如果它是原子指令是有意义的,因为这解释了更高的延迟和现有指令的全部意义。
猜你喜欢
  • 1970-01-01
  • 2013-10-11
  • 1970-01-01
  • 1970-01-01
  • 2014-03-16
  • 1970-01-01
  • 1970-01-01
  • 2012-02-14
  • 1970-01-01
相关资源
最近更新 更多