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

时间:2019-07-12 11:39:25

标签: c linux x86 atomicity memory-barriers

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

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

同时,在另一个CPU中,我使用了负载屏障来原子访问缓存行。我的期望是读取器(或使用者)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;
}

1 个答案:

答案 0 :(得分:4)

  

我使用了负载屏障来原子访问缓存行

否,障碍不会创造原子性。它们仅对您自己的操作进行排序,而不会阻止其他线程的操作出现在我们自己的两个线程之间。

当两个线程之间出现另一个线程的存储时,就会发生非原子性。 lfence不会采取任何行动来阻止这种情况。

lfence毫无意义;它只会使运行此线程的CPU停止运行,直到执行加载之前耗尽ROB / RS。 (lfence序列化执行,但对内存顺序没有影响,除非您使用的是WC内存中的NT加载,例如视频RAM。


您的选择是:

认识到这是一个X-Y问题,并且做一些不需要需要64字节原子加载/存储的操作。例如以原子方式将指针更新为非原子数据。一般情况是RCU,或者使用循环缓冲区的无锁队列。

  • 对于同意通过尊重该锁进行协作的线程,请使用软件锁来获得逻辑原子性(例如C11的_Atomic struct levels g_shared;)。

    如果读取的次数比更改的次数多,则SeqLock可能是一个不错的选择,或者特别是使用单个写入器和多个读取器的情况。读者可能会撕裂时重试。使用足够的内存顺序在读取之前/之后检查序列号。有关C ++ 11的实现,请参见Implementing 64 bit atomic counter with 32 bit atomics。 C11更容易,因为C允许从volatile结构分配给非volatile临时结构。

或硬件支持的64字节原子性:

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

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

  • 在CPU上的
  • AVX512(允许对整个缓存行进行加载或存储)恰好以使对齐的64字节加载/存储为原子的方式实现了。 除了lock cmpxchg16bmovdir64b之外,没有纸上保证在x86上任何超出8字节加载/存储范围的内容都是原子的。

    实际上,我们可以肯定的是,像Skylake这样的现代Intel CPU与AMD不同,它们在内核之间原子地传输了整个缓存行。而且我们知道,在英特尔(非AMD)上,不跨越高速缓存行边界的向量加载或存储确实可以单次访问L1d高速缓存,并在同一时钟周期内传输所有位。因此,在Skylake-avx512上对齐的vmovaps zmm, [mem]实际上应该是原子的,除非您拥有一个奇特的芯片组,该芯片组将许多插槽粘合在一起以产生撕裂的方式。 (多插槽K10与单插槽K10是一个很好的警示故事:Why is integer assignment on a naturally aligned variable atomic on x86?

  • MOVDIR64B-商店部分仅是原子的,并且仅在Intel Tremont(下一代Goldmont后继产品)上受支持。这仍然无法为您提供执行64字节原子加载的方法。而且它是一个绕过高速缓存的存储,因此不利于内核间通信延迟。我认为用例正在生成完整的PCIe事务。

另请参见SSE instructions: which CPUs can do atomic 16B memory operations?回复:缺少SIMD加载/存储的原子性保证。 CPU供应商出于某种原因未选择提供任何书面保证或方式来检测SIMD加载/存储何时是原子的,即使测试表明它们已在许多系统上(当您不跨越高速缓存行边界时)也是如此。 )