Cur*_*ous 7 c++ x86 assembly atomic cpu-architecture
我最近基准std::atomic::fetch_add
VSstd::atomic::compare_exchange_strong
在32核心SKYLAKE微架构的英特尔处理器。不出所料(根据我听说过的关于 fetch_add 的神话),fetch_add 的可扩展性几乎比 compare_exchange_strong 高一个数量级。看看程序的反汇编std::atomic::fetch_add
是用a实现的lock add
,std::atomic::compare_exchange_strong
是用lock cmpxchg
(https://godbolt.org/z/qfo4an)实现的。
是什么让lock add
英特尔多核处理器的速度如此之快?根据我的理解,两条指令的缓慢都来自缓存行上的争用,并且要以顺序一致性执行两条指令,正在执行的 CPU 必须以独占或修改模式(来自MESI)将该行拉入它自己的核心。那么处理器如何在内部优化 fetch_add 呢?
这是基准测试代码的简化版本。compare_exchange_strong 基准测试没有 load+CAS 循环,只有原子上的 compare_exchange_strong 和输入变量,该变量随着线程和迭代不断变化。所以这只是在多个 CPU 争用情况下的指令吞吐量的比较。
lock add
两者lock cmpxchg
的工作方式本质上是相同的,都是在微编码指令的持续时间内将该高速缓存线保持在修改状态。(num++ 对于 'int num' 可以是原子的吗?)。根据Agner Fog 的指令表,lock cmpxchg
和lock 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 add
,lock cmpxchg
例如也许让核心挂在线路上足够长的时间来执行几条lock add
指令。
你是这个意思吗?
或者也许您在微基准测试方法中遇到了一些重大失败,例如在开始计时之前可能没有进行预热循环以从空闲状态提高 CPU 频率?或者也许某些线程碰巧提前完成并让其他线程以较少的争用运行?