我正在尝试为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;
}
答案 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
AVX512(允许对整个缓存行进行加载或存储)恰好以使对齐的64字节加载/存储为原子的方式实现了。 除了lock cmpxchg16b
和movdir64b
之外,没有纸上保证在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加载/存储何时是原子的,即使测试表明它们已在许多系统上(当您不跨越高速缓存行边界时)也是如此。 )