我正在开发一个多核,多线程的软件库,我想在其中提供可能跨越多个缓存行的更新顺序保留无锁共享内存对象。
具体来说,假设我有一些向量X的高速缓存行大小的对象:X [0],…X [K]每个正好占据一条高速缓存行。我以索引顺序写入它们:首先是X [0],然后是X [1],依此类推。如果线程2读取X [K],它还会看到X [0]的状态“至少与当前一样”如X [K]所见?
显然,从同一线程,我将看到尊重更新顺序的内存语义。但是现在,如果第二个线程读取X [K],则会出现问题:是否会观察到对X [0] ... X [K-1]的相应更新?
通过锁定,我们确实获得了此保证。但是由于使用了memcpy来将某些内容复制到向量中,所以我们失去了此属性:memcpy具有POSIX语义,它不能保证索引顺序更新或内存顺序更新或任何其他顺序。您只需保证在完成memcpy之后,便完成了整个更新。
我的问题:是否已经有一个速度类似但有所需保证的订单保存memcpy?如果没有,那么可以在不锁定的情况下实现这种原语吗?
假设我的目标平台是x86和ARM。
(编者注:最初是Intel,因此OP可能并不关心AMD。)
答案 0 :(得分:6)
您描述的订购要求正是发布/获取语义所提供的。 (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?)。实际上,某些Intel CPU可能具有原子32或64字节(AVX512)存储,但是Intel和AMD均未正式做出任何保证。
我们甚至不知道SIMD向量存储区是否有保证的顺序,因为它们有可能将一个宽对齐的存储区分成多个8字节的对齐块。或者,即使这些块分别是原子的。 Per-element atomicity of vector load/store and gather/scatter?完全有理由相信它们是每个元素的原子,即使文档没有对此提供保证。
如果具有较大的“对象”对性能至关重要,则可以考虑在您关心的特定服务器上测试向量加载/存储原子性,但是就保证并让编译器使用它而言,您完全是一个人。 。 (有内在函数。)请确保您在不同插槽的内核之间进行测试,以捕获诸如SSE instructions: which CPUs can do atomic 16B memory operations?在8字节边界处撕裂的情况,这是由于K10 Opteron插槽之间的HyperTransport导致的。这可能是一个非常糟糕的主意。您无法猜测,在任何情况下,即使通常看起来像是原子的,任何微体系结构条件都可以使宽向量在非原子状态下存储为非原子。
您可以轻松地对数组元素(如
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上,它不会自动矢量化或其他任何功能,因为编译器不会优化原子,并且因为没有文档说明使用矢量来存储原子元素数组的连续元素是安全的。 :(因此,这基本上很糟。 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进行编译。在armv8-a处于AArch32模式下时,存储对是原子的。AArch64当然也具有原子8字节加载/存储。)
您必须避免使用常规的C库memcpy
实现。在x86上,它可以对大型副本使用弱排序的存储,从而可以在其自己的存储之间进行重新排序(但以后不能对其进行重新排序)不属于memcpy
的商店,因为这可能会破坏以后的发行商店。)
movnt
绕过高速缓存存储,或者具有ERMSB功能的CPU上的rep movsb
都可以产生这种效果。 Does the Intel Memory Model make SFENCE and LFENCE redundant?。
或者memcpy
实现可以简单地选择先进入最后一个(部分)向量,然后再进入其主循环。
在C和C ++中,对UB中非atomic
类型的并发写+读或写+写;这就是memcpy
拥有如此多的自由去做自己想做的事情的原因,包括使用弱排序的商店,只要有必要,它就使用sfence
来确保memcpy
整体上尊重排序编译器希望在以后的mo_release
操作中发出代码。
(即,当前x86的C ++实现会std::atomic
做这样的假设,即无需担心它们的弱排序存储。任何希望其NT存储遵守编译器生成的{{ 1}}代码必须使用atomic<T>
;或者,如果要手动编写asm,则直接使用_mm_sfence()
指令;或者,如果要进行顺序发布存储并提供asm,则只需使用sfence
也发挥xchg
的作用。)
答案 1 :(得分:1)
我发现此问题的 answer by Peter Cordes 很有见地、详细且非常有帮助。然而,我没有看到他的建议被写入代码,因此对于需要快速解决这个要求 DMA 或无锁算法的有序写入的问题的后代和未来的人们,我包括我根据该答案编写的代码。我在 x64 和 armv7-a 上使用 gcc 4.9 构建它,尽管我只在 x64 上运行并测试了它。
#include <atomic>
#include <stdlib.h>
#include <algorithm> // min
extern "C" {
static void * linear_memcpy_portable(void *__restrict dest, const void *__restrict src, size_t n)
{
// Align dest if not already aligned
if ((uintptr_t)dest & sizeof(uint64_t)) {
uint8_t *__restrict dst8 = reinterpret_cast<uint8_t *__restrict>(dest);
const uint8_t *__restrict src8 = reinterpret_cast<const uint8_t *__restrict>(src);
const size_t align_n = std::min(n, (uintptr_t)dest & sizeof(uint64_t));
const uint8_t * const endsrc8 = static_cast<const uint8_t * const>(src) + align_n;
while (src8 < endsrc8) {
*dst8 = *src8;
atomic_thread_fence(std::memory_order_release);
dst8++; src8++;
}
dest = dst8;
src = src8;
n = n - align_n;
}
typedef uint64_t __attribute__((may_alias,aligned(1))) aliasing_unaligned_uint64_t;
uint64_t *__restrict dst64 = static_cast<uint64_t *__restrict>(dest);
const aliasing_unaligned_uint64_t *__restrict src64 = static_cast<const aliasing_unaligned_uint64_t *__restrict>(src);
const uint64_t * const endsrc64 = src64 + n / sizeof(uint64_t);
const uint8_t * const endsrc8 = static_cast<const uint8_t * const>(src) + n;
while (src64 < endsrc64) {
*dst64 = *src64;
atomic_thread_fence(std::memory_order_release);
dst64++; src64++;
}
if (reinterpret_cast<const uint8_t * const>(endsrc64) != endsrc8) {
uint8_t *__restrict dst8 = reinterpret_cast<uint8_t *__restrict>(dst64);
const uint8_t *__restrict src8 = reinterpret_cast<const uint8_t *__restrict>(src64);
while (src8 < endsrc8) {
*dst8 = *src8;
atomic_thread_fence(std::memory_order_release);
dst8++; src8++;
}
}
return dest;
}
#if (_M_AMD64 || __x86_64__)
#include <immintrin.h>
static void * linear_memcpy_avx2(void *dest, const void * src, size_t n) __attribute__((target("avx2")));
static void * linear_memcpy_avx2(void *dest, const void * src, size_t n)
{
__m256i *__restrict dst256 = static_cast<__m256i *__restrict>(dest);
const __m256i *__restrict src256 = static_cast<const __m256i *__restrict>(src);
const __m256i * const endsrc256 = src256 + n / sizeof(__m256i);
const uint8_t * const endsrc8 = static_cast<const uint8_t *>(src) + n;
while (src256 < endsrc256) {
_mm256_storeu_si256(dst256, _mm256_loadu_si256(src256));
atomic_thread_fence(std::memory_order_release);
dst256++; src256++;
}
if (reinterpret_cast<const uint8_t * const>(endsrc256) != endsrc8)
linear_memcpy_portable(dst256, src256, endsrc8 - reinterpret_cast<const uint8_t * const>(endsrc256));
return dest;
}
static void * linear_memcpy_sse2(void *dest, const void * src, size_t n) __attribute__((target("sse2")));
static void * linear_memcpy_sse2(void *dest, const void * src, size_t n)
{
__m128i *__restrict dst128 = static_cast<__m128i *__restrict>(dest);
const __m128i *__restrict src128 = static_cast<const __m128i *__restrict>(src);
const __m128i * const endsrc128 = src128 + n / sizeof(__m128i);
const uint8_t * const endsrc8 = static_cast<const uint8_t *>(src) + n;
while (src128 < endsrc128) {
_mm_storeu_si128(dst128, _mm_loadu_si128(src128));
atomic_thread_fence(std::memory_order_release);
dst128++; src128++;
}
if (reinterpret_cast<const uint8_t * const>(endsrc128) != endsrc8)
linear_memcpy_portable(dst128, src128, endsrc8 - reinterpret_cast<const uint8_t * const>(endsrc128));
return dest;
}
static void *(*resolve_linear_memcpy(void))(void *, const void *, size_t)
{
__builtin_cpu_init();
// All x64 targets support a minimum of SSE2
return __builtin_cpu_supports("avx2") ? linear_memcpy_avx2 : linear_memcpy_sse2;
}
#ifdef __AVX2__
// IF AVX2 is specified to the compiler, alias to the avx2 impl so it can be inlined
void * linear_memcpy(void *, const void *, size_t) __attribute__((alias("linear_memcpy_avx2")));
#else
void * linear_memcpy(void *, const void *, size_t) __attribute__((ifunc("resolve_linear_memcpy")));
#endif
#else
void * linear_memcpy(void *, const void *, size_t) __attribute__((alias("linear_memcpy_portable")));
#endif
} // extern "C"
我欢迎任何关于实施的反馈。 :)