如何使用缓存行原子性在 CPU 之间复制多个数据元素?

how to copy multiple data elements between CPUs using cacheline atomicity?

我正在尝试为 CPU 之间的多个数据元素实现原子副本。我将多个数据元素打包到一个缓存行中,以原子方式操作它们。所以我写了下面的代码。

在此代码中,(使用 -O3 编译)我将全局结构数据对齐到单个缓存行中,并将元素设置在 CPU 中,后跟一个存储屏障。它是为了让其他人全局可见CPU.

同时,在另一个CPU中,我使用了一个加载屏障来原子地访问缓存行。我的期望是 reader(或消费者)CPU 应该将数据缓存行放入其自己的缓存层次结构 L1、L2 等中。因此,因为我不会再次使用负载屏障,直到下次读取时,数据的元素将是相同的,但它不会按预期工作。我无法在此代码中保持高速缓存行的原子性。作者 CPU 似乎将元素逐个放入缓存行中。怎么可能?

#include <emmintrin.h>
#include <pthread.h>
#include "common.h"

#define CACHE_LINE_SIZE             64

struct levels {
    uint32_t x1;
    uint32_t x2;
    uint32_t x3;
    uint32_t x4;
    uint32_t x5;
    uint32_t x6;
    uint32_t x7;
} __attribute__((aligned(CACHE_LINE_SIZE)));

struct levels g_shared;

void *worker_loop(void *param)
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(15, &cpuset);

    pthread_t thread = pthread_self();

    int status = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    fatal_relog_if(status != 0, status);

    struct levels shared;
    while (1) {

        _mm_lfence();
        shared = g_shared;

        if (shared.x1 != shared.x7) {
            printf("%u %u %u %u %u %u %u\n",
                    shared.x1, shared.x2, shared.x3, shared.x4, shared.x5, shared.x6, shared.x7);
            exit(EXIT_FAILURE);
        }
    }

    return NULL;
}

int main(int argc, char *argv[])
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(16, &cpuset);

    pthread_t thread = pthread_self();

    memset(&g_shared, 0, sizeof(g_shared));

    int status = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    fatal_relog_if(status != 0, status);

    pthread_t worker;
    int istatus = pthread_create(&worker, NULL, worker_loop, NULL);
    fatal_elog_if(istatus != 0);

    uint32_t val = 0;
    while (1) {
        g_shared.x1 = val;
        g_shared.x2 = val;
        g_shared.x3 = val;
        g_shared.x4 = val;
        g_shared.x5 = val;
        g_shared.x6 = val;
        g_shared.x7 = val;

        _mm_sfence();
        // _mm_clflush(&g_shared);

        val++;
    }

    return EXIT_SUCCESS;
}

输出如下

3782063 3782063 3782062 3782062 3782062 3782062 3782062

更新 1

我使用 AVX512 更新了如下代码,但问题仍然存在。

#include <emmintrin.h>
#include <pthread.h>
#include "common.h"
#include <immintrin.h>

#define CACHE_LINE_SIZE             64

/**
 * Copy 64 bytes from one location to another,
 * locations should not overlap.
 */
static inline __attribute__((always_inline)) void
mov64(uint8_t *dst, const uint8_t *src)
{
        __m512i zmm0;

        zmm0 = _mm512_load_si512((const void *)src);
        _mm512_store_si512((void *)dst, zmm0);
}

struct levels {
    uint32_t x1;
    uint32_t x2;
    uint32_t x3;
    uint32_t x4;
    uint32_t x5;
    uint32_t x6;
    uint32_t x7;
} __attribute__((aligned(CACHE_LINE_SIZE)));

struct levels g_shared;

void *worker_loop(void *param)
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(15, &cpuset);

    pthread_t thread = pthread_self();

    int status = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    fatal_relog_if(status != 0, status);

    struct levels shared;
    while (1) {
        mov64((uint8_t *)&shared, (uint8_t *)&g_shared);
        // shared = g_shared;

        if (shared.x1 != shared.x7) {
            printf("%u %u %u %u %u %u %u\n",
                    shared.x1, shared.x2, shared.x3, shared.x4, shared.x5, shared.x6, shared.x7);
            exit(EXIT_FAILURE);
        } else {
            printf("%u %u\n", shared.x1, shared.x7);
        }
    }

    return NULL;
}

int main(int argc, char *argv[])
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(16, &cpuset);

    pthread_t thread = pthread_self();

    memset(&g_shared, 0, sizeof(g_shared));

    int status = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    fatal_relog_if(status != 0, status);

    pthread_t worker;
    int istatus = pthread_create(&worker, NULL, worker_loop, NULL);
    fatal_elog_if(istatus != 0);

    uint32_t val = 0;
    while (1) {
        g_shared.x1 = val;
        g_shared.x2 = val;
        g_shared.x3 = val;
        g_shared.x4 = val;
        g_shared.x5 = val;
        g_shared.x6 = val;
        g_shared.x7 = val;

        _mm_sfence();
        // _mm_clflush(&g_shared);

        val++;
    }

    return EXIT_SUCCESS;
}

I used an load barrier to access the cacheline atomically

不,屏障不会创建原子性。他们只会命令你自己的操作,不会阻止来自其他线程的操作出现在我们自己的两个线程之间。

Non-atomicity 当另一个线程的存储在我们的两个负载之间变得可见时发生。 lfence没有阻止它。

lfence这里没有意义;它只是让 CPU 运行 这个线程停止,直到它在执行负载之前耗尽它的 ROB/RS 。 (lfence 序列化执行,但对内存排序没有影响,除非您使用来自 WC 内存的 NT 加载,例如视频 RAM)。


您的选择是:

认识到这是一个 X-Y 问题并做一些 不需要 需要 64 字节原子 loads/stores。例如自动将 指针 更新为 non-atomic 数据。一般情况是 RCU,或者可能是 lock-free 使用循环缓冲区的队列。

  • 使用软件锁为同意通过尊重锁合作的线程获得逻辑原子性(如 _Atomic struct levels g_shared; 与 C11)。

    如果读取次数多于更改次数,或者对于单个写入者和多个读取者,SeqLock 可能是此数据的不错选择。读者可能会撕裂时重试;检查序列号 before/after 读取,使用足够 memory-ordering。有关 C++11 实现,请参阅 ; C11 更容易,因为 C 允许从 volatile 结构分配给非 volatile 临时。

或hardware-supported 64字节原子性:

  • Intel 事务内存 (TSX) 在某些 CPU 上可用。这甚至会让你 对其执行原子 RMW,或者从一个位置原子地读取并写入另一个位置。但更复杂的交易更有可能中止。将 4x 16 字节或 2x 32 字节加载到事务中应该不会经常中止,即使在争用下也是如此。可以安全地将商店分组到一个单独的事务中。 (希望编译器足够聪明,可以在加载的数据仍在寄存器中的情况下结束事务,因此它也不必以原子方式存储到堆栈上的本地。)

    事务内存有 GNU C/C++ 扩展。 https://gcc.gnu.org/wiki/TransactionalMemory

  • AVX512(允许 full-cache-line 加载或存储)在 CPU 上恰好以对齐 64 字节 loads/stores 原子的方式实现它. 没有 on-paper 保证任何比 8 字节宽的 load/store 在 x86 上都是原子的,除了 lock cmpxchg16bmovdir64b.

    在实践中,我们相当确定像 Skylake 这样的现代英特尔 CPU 会在内核之间以原子方式传输整个 cache-lines,这与 AMD 不同。我们知道,在 Intel(不是 AMD)上,不跨越 cache-line 边界的矢量加载或存储确实会对 L1d 缓存进行单次访问,​​在同一时钟周期内传输所有位。因此,在 Skylake-avx512 上对齐的 vmovaps zmm, [mem] 实际上应该是原子的,除非你有一个奇异的芯片组,它以一种会产生撕裂的方式将许多插座粘合在一起。 (Multi-socket K10 vs. single-socket K10 是一个很好的警示故事:

  • MOVDIR64B - 存储部分只有原子,并且仅在 Intel Tremont(next-gen Goldmont 继任者)上受支持。这仍然没有为您提供执行 64 字节原子加载的方法。此外,它是一个 cache-bypassing 商店,因此不利于 inter-core 通信延迟。我认为 use-case 正在生成 full-size PCIe 事务。

另见 SSE instructions: which CPUs can do atomic 16B memory operations? 回复:SIMD 缺乏原子性保证 load/store。 CPU 供应商出于某种原因未选择提供任何书面保证或方法来检测 SIMD loads/stores 何时将是原子的,即使测试表明它们在许多系统上(当您不交叉时) cache-line 边界。)