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

问题描述 投票:6回答:2

我最近在32核Skylake Intel处理器上对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 cmpxchghttps://godbolt.org/z/qfo4an)实现。

是什么使intel多核处理器上的lock add如此之快?据我了解,两条指令的速度慢都来自于缓存行的争用,并且要以顺序一致的方式执行两条指令,执行中的CPU必须以排他或修改模式(从MESI起)将这条线拉入自己的内核。然后,处理器如何在内部优化fetch_add?


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

c++ assembly x86 atomic cpu-architecture
2个回答
2
投票

lock addlock cmpxchg两者都基本上以相同的方式工作,方法是在微码指令的持续时间内将其高速缓存行保持在“修改”状态。 (Can num++ be atomic for 'int num'?)。根据Agner Fog's指令表,lock cmpxchglock add是来自微代码的非常相似的oups数。 (尽管lock add稍微简单一些)。 Agner的吞吐量数字适用于无竞争的情况,在这种情况下,var在一个内核的L1d缓存中保持高温。缓存未命中可能会导致uop重播,但是我看不出有什么理由可以期望有明显的不同。

您声称您没有执行load + CAS或使用重试循环。但是,您是否仅指望成功的CAS或其他?在x86上,每个CAS(包括故障)的成本几乎与lock add相同。 (将所有线程都放在同一个原子变量上,使用expected的陈旧值会导致很多CAS失败。这不是CAS重试循环的常见用例。)>

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

您在问题中没有完整的代码,因此我不得不猜测,无法在我的桌面上尝试。您甚至没有任何性能计数器结果或类似的结果;有很多用于内核外内存访问的性能事件,例如mem_inst_retired.lock_loads之类的事件,可以记录执行的lock ed条指令的数量。

lock add,每当内核获得高速缓存行的所有权时,它都会成功进行递增。内核仅在等待对行访问的硬件仲裁,从不等待另一个内核获取该行,然后由于它具有过时的值而无法递增。


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

这是您的意思吗?


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


0
投票

要执行两条指令具有顺序一致性

© www.soinside.com 2019 - 2024. All rights reserved.