gpt4 book ai didi

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

转载 作者:塔克拉玛干 更新时间:2023-11-03 01:54:39 24 4
gpt4 key购买 nike

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

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

同时,在另一个 CPU 中,我使用负载屏障原子地访问缓存线。我的期望是读取器(或消费者)CPU 应该将数据缓存行带入其自己的缓存层次结构 L1、L2 等。因此,由于在下一次读取之前我不会再次使用负载屏障,因此数据的元素将是相同的,但它不能按预期工作。我不能在这段代码中保持缓存线的原子性。编写器 CPU 似乎将元素一 block 一 block 地放入缓存线。怎么可能?

#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



不,屏障不会产生原子性 .他们只命令您自己的操作,而不是阻止其他线程的操作出现在我们自己的两个线程之间。

当另一个线程的存储在我们的两个负载之间变得可见时,就会发生非原子性。 lfence无能为力。
lfence这是没有意义的;它只会使运行该线程的 CPU 停止,直到它在执行加载之前耗尽其 ROB/RS。 ( lfence 序列化执行,但对内存排序没有影响,除非您使用来自 WC 内存的 NT 加载,例如视频 RAM)。

您的选择是:

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

或者
  • 使用软件锁来获得逻辑原子性(如 _Atomic struct levels g_shared; 使用 C11),以便同意通过尊重锁进行合作的线程。

    如果读取次数多于更改次数,则 SeqLock 可能是该数据的不错选择 ,或者特别是对于单个作者和多个读者。读者在可能撕裂时重试;使用足够的内存排序在读取之前/之后检查序列号。见 Implementing 64 bit atomic counter with 32 bit atomics对于 C++11 实现; 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 cmpxchg16b 之外,没有纸上保证任何比 8 字节加载/存储更宽的东西在 x86 上都是原子的。和 movdir64b .

    在实践中,我们相当确定像 Skylake 这样的现代英特尔 CPU 在内核之间以原子方式传输整个缓存线,这与 AMD 不同。而且我们知道,在 Intel(不是 AMD)上,一个不跨越缓存线边界的 vector 加载或存储确实会对 L1d 缓存进行单次访问,​​在同一时钟周期内传输所有位。所以对齐 vmovaps zmm, [mem]在 Skylake-avx512 上实际上应该是原子的,除非你有一个奇特的芯片组,它以一种会造成撕裂的方式将许多插槽粘合在一起。 (多插槽 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 加载/存储何时是原子的,即使测试表明它们在许多系统上(当您不跨越缓存线边界时)。 )

    关于c - 如何使用缓存线原子性在 CPU 之间复制多个数据元素?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57006271/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com