如何在 x86 处理器上实现“加锁”

Cur*_*ous 7 c++ x86 assembly atomic cpu-architecture

我最近基准std::atomic::fetch_addVSstd::atomic::compare_exchange_strong在32核心SKYLAKE微架构的英特尔处理器。不出所料(根据我听说过的关于 fetch_add 的神话),fetch_add 的可扩展性几乎比 compare_exchange_strong 高一个数量级。看看程序的反汇编std::atomic::fetch_add是用a实现的lock addstd::atomic::compare_exchange_strong是用lock cmpxchghttps://godbolt.org/z/qfo4an)实现的。

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


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

Pet*_*des 7

lock add两者lock cmpxchg 的工作方式本质上是相同的,都是在微编码指令的持续时间内将该高速缓存线保持在修改状态。(num++ 对于 'int num' 可以是原子的吗?)。根据Agner Fog 的指令表,lock cmpxchglock add是微代码中微指令的数量非常相似。(虽然lock add稍微简单一些)。Agner 的吞吐量数据适用于无竞争情况,其中 var 在一个核心的 L1d 缓存中保持热状态。缓存未命中可能会导致 uop 重播,但我认为没有任何理由预期会出现显着差异。

您声称您没有执行 load+CAS 或使用重试循环。但有没有可能你只计算成功的 CAS 或者其他什么?在 x86 上,每个 CAS(包括故障)的成本几乎与lock add. (由于所有线程都在同一个原子变量上进行操作,因此使用过时的值会导致大量 CAS 失败expected。这不是 CAS 重试循环的常见用例)。

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

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

使用lock add,每次核心获得缓存行的所有权时,它都会成功执行增量操作。内核仅等待对线路访问的硬件仲裁,而不会等待另一个内核获取该线路,然后由于该线路的值已过时而无法递增。


硬件仲裁可能会以不同的方式对待lock addlock cmpxchg例如也许让核心挂在线路上足够长的时间来执行几条lock add指令。

你是这个意思吗?


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