8

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

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


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

4

2 回答 2

5

lock add并且lock cmpxchg 两者的工作方式基本相同,通过在微编码指令的持续时间内保持处于修改状态的高速缓存行。(对于“int num”,num++ 可以是原子的吗?)。根据Agner Fog 的指令表,lock cmpxchglock add微码中的微指令数量非常相似。(虽然lock add稍微简单一些)。Agner 的吞吐量数字适用于无竞争情况,其中 var 在一个内核的 L1d 缓存中保持热状态。缓存未命中可能会导致 uop 重播,但我看不出有任何理由期望会有显着差异。

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

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

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

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


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

你是这个意思吗?


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

于 2019-12-31T04:51:23.423 回答
2

要以顺序一致性执行两条指令,正在执行的 CPU 必须以独占或修改模式(来自 MESI)将线路拉入它自己的内核。

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

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

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

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

于 2020-01-01T00:34:36.560 回答