我正在尝试了解 x86/x64 上 CAS 的低级机制,我非常感谢一些帮助/见解。
我一直在考虑这个问题的原因是我试图对指数退避进行推理,并从原则上找出正确的退避延迟单位应该是多少。
如果我查看一个没有指数退避的无锁 freelist 基准测试,我会看到随着线程数量的增加,性能迅速趋于平缓。
Release 7 Lock-Free Freelist Benchmark #1
M
N
S
L3U
L2U L2U
L1D L1D
L1I L1I
P P
L L L L total ops,mean ops/sec per thread,standard deviation,scalability
0 0 0 1 310134488,31013449,0,1.00
0 1 0 1 136313300,6815665,38365,0.22
0 1 0 1 136401284,6820064,50706,0.22
1 1 1 1 111134328,2778358,23851,0.09
0 0 1 1 334747444,16737372,2421,0.54
1 1 1 1 111105898,2777647,40399,0.09
正如我们所知,可能会发生活锁,每个线程都会阻止其他线程继续进行。
我最初的想法——现在我认为是错误的——认为 CAS 正在干扰 CAS。我的意思是,如果它们同时发生,CAS 指令本身将与另一个 CAS 发生破坏性冲突。两者都会失败。(Prolly,因为我在脑海中思考以太网)。
这“显然”解释了结果 - 所有这些 CAS 指令同时运行,很少有机会在被破坏性中断之前完全执行。
再想一想,我相信现在情况并非如此。CAS 指令实际上没有故障模式。它会告诉你目的地等于或不等于比较对象。就这样。它不会回来说“哦,对不起,撞到别人了”。
破坏性干扰正在发生,但它发生在更高级别,在数据结构算法本身中。当我们从/向自由列表推送或弹出时,我们实际上是在尝试交换。我们需要目标足够长的时间稳定,以便我们可以读取它,做我们需要做的任何工作,然后发现它没有改变,这样我们就可以完成我们的推送/弹出。
如果其他线程继续 CASing,destination 就不稳定——它一直在变化——我们一直不得不重试我们的操作。
但现在我很困惑。
我们看到的是,单个线程执行了大约 3000 万次 push/pop 操作。Destination 必须在其中一项操作期间保持稳定,操作才能成功,因此我们看到有 3000 万个“槽”。如果我们有两个线程,那么我们可以拥有的最大理论性能是每个线程 1500 万次操作;每个线程使用一半的插槽。
现在让我们回到 CAS。CAS 没有故障模式。那么当另一个线程已经在进行 CAS 时,当第二个线程尝试 CAS 时会发生什么?好吧,第二个线程将在数据结构级别失败,因为交换无法发生,所以它将重试交换。
但现在想象我们有很多线程。开始 CAS 的第一个线程将成功(假设每个 CAS 花费完全相同的时间 - 不正确,但该假设不会改变任何基本内容,因此可以推理)。所有其他人都会失败。
但是一旦第一个线程完成,读取新目标值的下一个线程将使其 CAS 成功(所有其他线程,仍在执行它们的 CAS 或现在开始新的 CAS,将失败)。
那么为什么我们看不到完美的缩放比例呢?因为每个“插槽”都应该被使用!
我认为因此我不正确理解CAS。
阅读英特尔的架构软件开发人员手册,我发现如果所有数据都存在于缓存中(我感兴趣的情况),缓存一致性协议会处理 CAS。
Drepper 在他的白皮书中描述了 LL/SC 以及它是如何使用 MESI 工作的。
对我来说,CAS 以类似的方式运作似乎是合理的。
让我们考虑两个线程的情况。第一个线程开始其 CAS。具有目标的缓存行在其缓存中并标记为独占。
第二个线程开始 CAS。第一个核心将其缓存线发送到第二个核心,并且两个核心都将该缓存线标记为共享。
第一个线程完成 CAS 并写入缓存行(写入总是发生在 x86/x64 上,即使比较结果为假;它只是写入原始值)。
写入动作将缓存行标记为已修改;发生 RFO,导致第二个内核将其缓存行标记为无效。
第二个线程来完成它的 CAS 并注意到它的缓存行是无效的......然后,什么?我发现很难相信指令在 CPU 内部循环,直到它成功 - 尽管我想知道,因为 ARM 上的 LL/SC 要求您在程序集中执行此循环。但是CAS指令知道destination的值发生了变化,所以它的比较结果是无效的。但是 CAS 不会出错;它总是返回 true 或 false 进行比较。但即使指令确实循环直到完成,我仍然期望完美的缩放。每个“插槽”仍应使用。
那么会发生什么?CAS 发生了什么?
我所看到的是,随着线程数的增加,完成的工作越来越少——所有可用的“插槽”肯定都没有被使用。是什么原因造成的。CAS指令之间是否存在破坏性干扰?还是大量 RFO 占用 CPU-> 北桥总线?
我非常感兴趣地注意到的是,同一物理核心上的两个线程可以完美扩展。在这种情况下会发生一些特殊和不同的事情——不同物理内核上的两个线程也可以缩放一半。但这还不足以解释这一切。