c++ - C++ 中的顺序保留 memcpy

标签 c++ x86 arm memcpy lock-free

我正在开发一个多核、多线程的软件库,我想在其中提供可能跨越多个缓存行的更新顺序保留无锁共享内存对象。

具体来说,假设我有一些缓存行大小对象的 vector X:X[0], ... X[K] 每个都占用一个缓存行。我按索引顺序写入它们:首先是 X[0],然后是 X[1],等等。如果线程 2 读取 X[K],它是否还会看到 X[0] 的状态“至少是当前”就像它看到的 X[K] 一样?

从同一个线程,显然我会看到尊重更新顺序的内存语义。但是现在,如果某个第二个线程读取 X[K],问题就会出现:是否会观察到 X[0]...X[K-1] 的相应更新?

通过锁定,我们确实得到了这种保证。但是使用 memcpy 将某些内容复制到 vector 中时,我们失去了这个属性:memcpy 具有 POSIX 语义,它根本不保证索引顺序更新或内存顺序更新或任何其他排序。您只需保证在 memcpy 完成后,整个更新都已执行。

我的问题:是否已经有一个具有类似速度但具有所需保证的顺序保留 memcpy?如果没有,这样的原语可以在没有锁定的情况下实现吗?

假设我的目标平台是 x86 和 ARM。

(编者注:原来是说Intel,所以OP可能不关心AMD。)

最佳答案

您描述的订购要求正是发布/获取语义所提供的。 (http://preshing.com/20120913/acquire-and-release-semantics/)。

问题在于,在所有 x86 和某些 ARM 上,有效保证原子加载/存储的原子性单位最多为 8 个字节。否则在其他 ARM 上只有 4 个字节。 (Why is integer assignment on a naturally aligned variable atomic on x86?)。一些英特尔 CPU 在实践中可能有原子 32 甚至 64 字节 (AVX512) 存储,但英特尔和 AMD 都没有做出任何官方保证。

当 SIMD vector 存储可能将宽对齐存储分解为多个 8 字节对齐块时,我们甚至不知道它们是否有保证的顺序。或者即使这些块是单独的原子。 Per-element atomicity of vector load/store and gather/scatter? 有充分的理由相信它们是每个元素的原子,即使文档不保证。

如果拥有大型“对象”对性能至关重要,您可以考虑在您关心的特定服务器上测试 vector 加载/存储原子性,但就保证和让编译器使用它而言,您完全靠自己。 (有内在函数。)确保在不同插槽上的内核之间进行测试,以捕获SSE instructions: which CPUs can do atomic 16B memory operations?等由于 K10 Opteron 上的插槽之间的 HyperTransport 而撕裂 8 字节边界的情况。这可能是一个非常糟糕的主意;您无法猜测是否有任何微体系结构条件可以使宽 vector 存储在极少数情况下成为非原子的,即使它通常看起来是原子的。

您可以轻松地对数组元素进行发布/获取排序,例如alignas(64) atomic<uint64_t> arr[1024];你只需要很好地问编译器:

copy_to_atomic(std::atomic<uint64_t> *__restrict dst_a, 
                      const uint64_t *__restrict src, size_t len) {
    const uint64_t *endsrc = src+len;
    while (src < src+len) {
        dst_a->store( *src, std::memory_order_release );
        dst_a++; src++;
    }
}

在 x86-64 上,它不会自动矢量化或任何东西,因为编译器不会优化原子,并且因为没有文档表明使用 vector 来存储原子元素数组的连续元素是安全的。 :(所以这基本上很糟糕。看到它on the Godbolt compiler explorer

我会考虑使用volatile __m256i*指针(对齐加载/存储)和编译器屏障(如atomic_thread_fence(std::memory_order_release))滚动你自己的指针,以防止编译时重新排序。每个元素的排序/原子性应该没问题(但同样不能保证)。并且绝对不要指望整个 32 字节都是原子的,只是在较低的uint64_t元素之后写入较高的uint64_t元素(并且这些存储按该顺序对其他内核可见)。

在 ARM32 上:即使是uint64_t的原子存储也不是很好。 gcc 使用ldrexd/strexd对(LL/SC),因为显然没有 8 字节原子纯存储。 (我用 gcc7.2 -O3 -march=armv7-a 编译。在 AArch32 模式下使用 armv8-a,store-pair 是原子的。AArch64 当然也有原子的 8 字节加载/存储。)

您必须避免使用普通的 C 库memcpy实现。 在 x86 上,它可以对大拷贝使用弱排序的存储,允许在它自己的存储之间重新排序(但不能与不属于memcpy的后期存储进行重新排序,因为这可能会破坏后期发布的存储。)

vector 循环中的movnt缓存绕过存储,或具有 ERMSB 功能的 CPU 上的rep movsb,都可能产生这种效果。 Does the Intel Memory Model make SFENCE and LFENCE redundant?

或者memcpy实现可以简单地选择在进入其主循环之前先执行最后一个(部分) vector 。

C 和 C++ 中 UB 中非atomic类型的并发写+读或写+写;这就是为什么memcpy可以自由地做任何它想做的事情,包括使用弱排序的存储,只要它在必要时使用sfence以确保memcpy作为一个整体尊重编译器在发出时所期望的顺序稍后mo_release操作的代码。

(即 x86 的当前 C++ 实现执行std::atomic,假设没有弱排序存储需要他们担心。任何希望其 NT 存储尊重编译器生成的atomic<T>代码顺序的代码必须使用_mm_sfence() 。或者如果是手工编写asm,直接使用sfence指令。或者如果你想做一个顺序释放存储并给你的asm函数一个xchg的效果,或者只是使用atomic_thread_fence(mo_seq_cst)。)

关于c++ - C++ 中的顺序保留 memcpy,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52043182/

相关文章:

c++ - Borland x86 内联汇编程序;获取标签的地址?

delphi - 如何用SSE2优化这个Delphi函数?

x86 - 在英特尔酷睿 i3/i7 的情况下,从缓存集驱逐后数据的去向

Android - 动态查看arm版本?

java - 在 CORBA 客户端/服务器应用程序中将 unsigned long(从 C++)分配给 long(Java)?

c++ - 命名空间搜索限定名称的规则是什么?

arm - 试图了解STM32L4的ADC

c++ - 在 Android NDK 的 C++ arm 中出现缩小转换错误

c++ - 提升测试无法找到自定义打印

c++ - 此代码行推断出什么?