【问题标题】:How "lock add" is implemented on x86 processors如何在 x86 处理器上实现“加锁”
【发布时间】:2020-04-19 12:26:21
【问题描述】:

我最近在 32 核 Skylake 英特尔处理器上对 std::atomic::fetch_addstd::atomic::compare_exchange_strong 进行了基准测试。不出所料(从我听说过的有关 fetch_add 的神话中),fetch_add 的可扩展性几乎比 compare_exchange_strong 高一个数量级。看程序的反汇编std::atomic::fetch_add是用lock add实现的,std::atomic::compare_exchange_strong是用lock cmpxchg实现的(https://godbolt.org/z/qfo4an)。

是什么让lock add 在英特尔多核处理器上运行得如此之快?据我了解,两条指令的缓慢来自缓存线的争用,并且要以顺序一致性执行两条指令,执行的 CPU 必须以独占或修改模式将这条线拉入它自己的核心(来自MESI)。那么处理器如何在内部优化 fetch_add 呢?


This 是基准测试代码的简化版本。 compare_exchange_strong 基准测试没有 load+CAS 循环,只有 atomic 上的 compare_exchange_strong 输入变量不断随线程和迭代而变化。所以这只是多个 CPU 争用下指令吞吐量的比较。

【问题讨论】:

  • 您应该展示您的基准测试代码,因为它通常是问题所在。请注意,即使没有使用 LOCK 前缀,您也会遇到相同的缓存争用问题,因为 CPU 必须将缓存线拉入内存以进行任何小于完整缓存线宽度的写入。
  • @RossRidge 使用基准测试代码编辑了问题。
  • 你能用实际数字(iter/s)发布实际的循环反汇编吗?
  • 你并没有真正对 fetch_add() 进行基准测试。因为您没有使用获取的值,所以编译器将其优化为原子的“不获取,只需添加”,而您基准测试的正是这个“不获取,只需添加”。
  • 您可以将锁定指令视为采用内存互斥锁:内存系统中可以被内存访问系统锁定的最小部分的互斥锁。它的工作方式保证不会发生死锁,并且锁不必是乐观的(它永远不必被取消)。

标签: c++ assembly x86 atomic cpu-architecture


【解决方案1】:

要执行两个指令具有顺序一致性, 执行 CPU 必须以独占方式将线路拉入它自己的核心 修改模式(来自 MESI)。

不,要使用任何一致的、已定义的语义来执行任何一条指令,以保证多个 CPU 上的并发执行不会丢失增量,您需要这样做。即使您愿意放弃“顺序一致性”(根据这些说明),甚至放弃通常的读写保证。

任何锁定指令都有效地在足以保证原子性的内存部分强制执行互斥。 (与常规互斥锁类似,但在内存级别。)由于在操作期间没有其他内核可以访问该内存范围,因此原子性得到了微不足道的保证。

是什么让英特尔多核处理器上的加锁速度如此之快?

我希望在这些情况下,任何微小的时​​间差异都是至关重要的,并且执行加载加比较(或比较加载加比较加载......)可能会改变时间足以失去机会,就像太当存在大量争用并且访问模式的微小变化会改变互斥锁的归属方式时,使用互斥锁的代码的效率可能会大不相同。

【讨论】:

    【解决方案2】:

    lock addlock cmpxchg 的工作方式基本相同,通过在微编码指令的持续时间内保持处于修改状态的高速缓存行。 (Can num++ be atomic for 'int num'?)。根据Agner Fog's 指令表,lock cmpxchglock add 是来自微码的非常相似的微指令数。 (虽然lock add 稍微简单一些)。 Agner 的吞吐量数字适用于无竞争情况,其中 var 在一个内核的 L1d 缓存中保持热状态。缓存未命中可能会导致 uop 重播,但我认为没有任何理由可以预期会有显着差异。

    您声称您没有执行加载+CAS 或使用重试循环。但是有没有可能你只计算成功的 CAS 或其他什么?在 x86 上,每个 CAS(包括失败)的成本几乎与 lock add 相同。 (由于您的所有线程都在同一个原子变量上敲击,您将因使用 expected 的陈旧值而导致大量 CAS 失败。这不是 CAS 重试循环的常见用例)。

    或者您的 CAS 版本实际上是从原子变量中进行纯加载以获得expected 值吗?这可能会导致内存顺序错误推测。

    您的问题中没有完整的代码,所以我不得不猜测,并且无法在我的桌面上尝试。你甚至没有任何性能计数器结果或类似的东西;有很多用于内核外内存访问的 perf 事件,以及像 mem_inst_retired.lock_loads 这样的事件可以记录执行的 locked 指令的数量。

    使用lock add,每次核心获得缓存行的所有权时,它都会成功地进行增量。核心只等待硬件仲裁对线路的访问,而不是等待另一个核心获得线路然后因为它有一个陈旧的值而无法递增。


    硬件仲裁可以区别对待lock addlock cmpxchg 是合理的,例如也许让一个核心在线路上挂起足够长的时间来执行几个lock add 指令。

    你是这个意思吗?


    或者,也许您在微基准测试方法中遇到了一些重大故障,例如在开始计时之前可能没有进行预热循环以使 CPU 频率从空闲状态上升?或者也许某些线程碰巧提前完成,让其他线程以较少的争用运行?

    【讨论】:

      猜你喜欢
      • 2014-12-09
      • 1970-01-01
      • 2013-09-11
      • 1970-01-01
      • 1970-01-01
      • 2011-08-11
      • 1970-01-01
      • 2021-01-12
      • 2017-02-07
      相关资源
      最近更新 更多