C++ 原子和带有 RDMA 的内存顺序

Ken*_*man 5 c++ cpu-architecture memory-barriers rdma stdatomic

当在现代无锁内存上使用单侧 RDMA 时,会出现这样的问题:如果数据对象跨越多个缓存行,远程读取器如何安全地查看其传入数据。

\n

在 Derecho 开源多播和复制日志库(位于https://GitHub.com/Derecho-Project上)中,我们有这种模式。写入器 W 被授予写入读取器 R 中的一系列内存的权限。内存已正确固定和映射。现在,假设写入涉及跨越许多缓存行的某种数据向量,这很常见。我们使用一个守卫:一个递增的计数器(也在 RDMA 可访问内存中,但在其他一些缓存行中)。R 旋转,当它看到变化时观察计数器\xe2\x80\xa6,这告诉 R \xe2\x80\x9c 你有一条新消息\xe2\x80\x9d,然后 R 读取向量中的数据。后来我们有第二种模式,R 对 W 说,\xe2\x80\x9c我已经处理完该消息,你可以发送另一条消息。\xe2\x80\x9d

\n

我的问题:对于现代内存模型,应该使用哪种 C++ 原子风格来写入向量的内存?这会被称为宽松一致性吗?我希望我的代码能够在 ARM 和 AMD 上运行,而不仅仅是具有强大 TSO 内存模型的英特尔。

\n

那么对于我的计数器,当 R 旋转监视计数器更新时,我希望如何声明计数器?是否需要将其声明为获取-释放原子?

\n

最后,在 R 观察到计数器已增加之后,就速度或正确性而言,将所有内容声明为宽松的,然后在此处使用内存顺序栅栏是否有任何优点?我的想法是,通过第二种方法,我在所有 RDMA 内存上使用最小一致性模型(并对所有此类内存使用相同的模型),而且我只需要在观察到计数器增加后调用成本更高的内存顺序栅栏。因此,在访问我的向量之前,它只发生一次,而每次我的轮询线程循环时,获取释放原子计数器都会触发内存防护机制。对我来说,这听起来非常昂贵。

\n

最后一个想法又引出了一个问题:我是否也必须将此内存声明为易失性,以便 C\xe2\x80\x94 编译器意识到数据可以在其脚下更改,或者编译器本身可以看到数据就足够了std::原子类型声明?在Intel上,对于全店订购,肯定需要TSO加上易失性。

\n

[编辑:新信息](我试图在这里吸引一些帮助!)

\n

一种选择似乎是将 RDMA 内存区域声明为 std::atomic<relaxed_consistency> 但每次我们的谓词评估线程重新测试防护时都使用锁(在 RDMA 内存中,将使用相同的宽松属性进行声明) )。我们将保留 C++ 易失性注释。

\n

原因是,使用具有获取-释放语义的锁,内存一致性硬件将被警告它需要隔离先前的更新。锁本身(互斥体)可以声明为谓词线程本地的,然后将存在于本地 DRAM 中,这是便宜的,并且由于这不是任何东西争用的锁,因此锁定它可能与 test_and_set 一样便宜,并且解锁只是写入 0。如果谓词为 true,我们的触发代码体将在访问锁之后运行(可能是在锁释放之后),因此我们建立所需的顺序以确保硬件将获取受保护的对象使用实际的内存读取。但是,通过谓词测试的每个周期(每次“旋转”),我们最终都会对每个谓词执行锁定获取/释放。所以这会导致一些速度减慢。

\n

选项二看似开销较小,也将 RDMA 区域声明为具有宽松一致性的 std::atomic,但省略了锁并像我们现在一样进行测试。然后,当谓词测试为真时,我们将使用语义执行显式内存栅栏(std::memory-order)。我们得到相同的屏障,但仅在谓词评估为 true 时才支付成本,因此开销更少。

\n

但现在我们遇到了一个不同类型的问题。Intel 有总存储顺序 TSO,并且由于任何线程都会执行一些先写后读操作,Intel 可能被迫出于预防措施从内存中获取保护变量,担心 TSO 可能会被违反。具有 volatile 的 C++ 肯定包含 fetch 指令。但在 ARM 和 AMD 上,硬件本身是否有可能在硬件寄存器或其他东西中长时间存储某些保护变量,从而导致我们的“类自旋”循环出现极端延迟?对ARM和AMD一无所知,这似乎令人担忧。但也许你们中有人比我知道得多?

\n

Ken*_*man 0

嗯,目前在这个问题上似乎缺乏专业知识。也许 std::atomics 选项的新颖性以及 ARM 和 AMD 将如何实现宽松一致性的普遍不确定性使人们很难知道答案,猜测也没有帮助。

据我了解,正确的答案似乎是:

  1. 由于英特尔的 TSO(总商店订单)政策,整个问题不会出现在英特尔身上。对于 TSO,因为守卫在它所守卫的向量之后更新,因此在任何总存储顺序中,守卫都是最后更新的。看到保护变化可以保证接收者将看到更新的向量元素。此外,AMD 和 ARM 上的默认设置很可能模仿 TSO。
  2. 通过显式声明 RDMA 内存区域具有relaxed_consistency,开发人员可以选择更便宜的内存模型,但有义务插入内存栅栏。最明显的方法是在读取防护之前获取锁,然后在执行此操作后释放锁。即使没有其他线程争用锁,这也会产生成本。首先,锁定操作本身需要几个时钟周期。但更广泛地说,锁定随机互斥锁将对缓存产生一些未知的影响,因为硬件必须假设锁实际上已被争夺,可能已经发生等待,并且值可能已经在其脚下发生了变化。这将导致需要量化的成本。
  3. 同样,可以将守卫声明为使用 acquire_release 一致性。看起来,这会创建一个内存栅栏,并且任何看到保护值更改的读者都可以看到用于写入向量的先前更新。再次强调,成本需要量化。
  4. 也许,可以在谓词触发的代码块的顶部进行围栏读取。这将使栅栏脱离主谓词循环,因此栅栏的成本只会支付一次,并且仅在谓词实际上为真时支付。

我们还需要在 C++ 中将原子标记为 volatile。事实上,C++ 可能应该注意到何时访问 std::atomic 类型,并将其视为对 volatile 的访问。然而,目前 C++ 编译器正在实施这一策略并不明显。