memcpy 在 linux 中移动 128 位

memcpy moving 128 bit in linux

我正在 linux 中为 PCIe 设备编写设备驱动程序。该设备驱动程序执行多次读写以测试吞吐量。当我使用 memcpy 时,TLP is 8 bytes ( on 64 bits architectures ). In my opinion the only way to get a payload of 16 bytes is to use the SSE instruction set. I've already seen this 的最大有效载荷但代码无法编译( AT&T/Intel 语法问题)。

首先,您可能使用 GCC 作为编译器,它使用 asm 语句作为内联汇编程序。使用它时,您必须为汇编代码使用字符串文字(在发送到汇编程序之前将其复制到汇编代码中 - 这意味着该字符串应包含换行符)。

其次,您可能不得不为汇编程序使用 AT&T 语法。

第三个 GCC 使用 extended asm 在汇编器和 C 之间传递变量。

第四,无论如何,您应该尽可能避免使用内联汇编程序,因为编译器不可能将指令安排到 asm 语句之后(至少这是真的)。相反,您可以使用 GCC 扩展,例如 vector_size 属性:

typedef float v4sf __attribute__((vector_size(16)));

void fubar( v4sf *p, v4sf* q )
{
  v4sf p0 = *p++;
  v4sf p1 = *p++;
  v4sf p2 = *p++;
  v4sf p3 = *p++;

  *q++ = p0;
  *q++ = p1;
  *q++ = p2;
  *q++ = p3;
}

的优点是,即使您为没有 mmx 寄存器但可能是其他一些 128 位寄存器(或没有矢量寄存器)的处理器编译,编译器也会生成代码完全没有)。

第五,您应该调查所提供的 memcpy 是否不够快。通常 memcpy 确实优化了。

第六,如果您在 Linux 内核中使用特殊寄存器,您应该采取预防措施,有些寄存器在上下文切换期间不会被保存。 SSE 寄存器是其中的一部分。

第七,当你使用它来测试吞吐量时,你应该考虑处理器是否是等式中的一个重要瓶颈。将代码的实际执行与对 RAM 的读取 from/writes(您命中还是未命中缓存?)或对外围设备的读取 from/write 进行比较。

第八,在移动数据时,您应该避免将大块数据从 RAM 移动到 RAM,如果它是 to/from 带宽有限的外围设备,您绝对应该考虑为此使用 DMA。请记住,如果访问时间限制了性能,CPU 仍将被视为繁忙(尽管它不能 运行 以 100% 的速度)。

link you mentioned is using non-temporal stores. I have discussed this several times before, for example here and 。我建议您在继续之前先阅读这些内容。

但是,如果您真的想在 link 中生成内联汇编代码,您在此处提到的是如何实现:改用内部函数。

您无法使用 GCC 编译该代码的事实正是创建内部函数的原因之一。必须针对 32 位和 64 位代码以不同方式编写内联汇编,并且每个编译器通常具有不同的语法。 Intrinsics 解决了所有这些问题。

以下代码应在 32 位和 64 位模式下使用 GCC、Clang、ICC 和 MSVC 进行编译。

#include "xmmintrin.h"
void X_aligned_memcpy_sse2(char* dest, const char* src, const unsigned long size)
{
    for(int i=size/128; i>0; i--) {
        __m128i xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7;
        _mm_prefetch(src + 128, _MM_HINT_NTA);
        _mm_prefetch(src + 160, _MM_HINT_NTA);
        _mm_prefetch(src + 194, _MM_HINT_NTA);
        _mm_prefetch(src + 224, _MM_HINT_NTA);

        xmm0 = _mm_load_si128((__m128i*)&src[   0]);
        xmm1 = _mm_load_si128((__m128i*)&src[  16]);
        xmm2 = _mm_load_si128((__m128i*)&src[  32]);
        xmm3 = _mm_load_si128((__m128i*)&src[  48]);
        xmm4 = _mm_load_si128((__m128i*)&src[  64]);
        xmm5 = _mm_load_si128((__m128i*)&src[  80]);
        xmm6 = _mm_load_si128((__m128i*)&src[  96]);
        xmm7 = _mm_load_si128((__m128i*)&src[ 112]);

        _mm_stream_si128((__m128i*)&dest[   0], xmm0);
        _mm_stream_si128((__m128i*)&dest[  16], xmm1);
        _mm_stream_si128((__m128i*)&dest[  32], xmm2);
        _mm_stream_si128((__m128i*)&dest[  48], xmm3);
        _mm_stream_si128((__m128i*)&dest[  64], xmm4);
        _mm_stream_si128((__m128i*)&dest[  80], xmm5);
        _mm_stream_si128((__m128i*)&dest[  96], xmm6);
        _mm_stream_si128((__m128i*)&dest[ 112], xmm7);
        src  += 128;
        dest += 128;
    }
}

注意 srcdest 需要 16 字节对齐,size 需要是 128 的倍数。

但是,我不建议使用此代码。在非临时存储有用的情况下,循环展开是无用的,显式预取很少有用。你可以简单地做

void copy(char *x, char *y, int n)
{
    #pragma omp parallel for schedule(static)
    for(int i=0; i<n/16; i++) {
        _mm_stream_ps((float*)&y[16*i], _mm_load_ps((float*)&x[16*i]));
    }
}

有关原因的更多详细信息,请参见


这是 X_aligned_memcpy_sse2 函数的程序集,使用了 GCC -O3 -S -masm=intel 的内在函数。请注意,它与 here.

基本相同
    shr rdx, 7
    test    edx, edx
    mov eax, edx
    jle .L1
.L5:
    sub rsi, -128
    movdqa  xmm6, XMMWORD PTR [rsi-112]
    prefetchnta [rsi]
    prefetchnta [rsi+32]
    prefetchnta [rsi+66]
    movdqa  xmm5, XMMWORD PTR [rsi-96]
    prefetchnta [rsi+96]
    sub rdi, -128
    movdqa  xmm4, XMMWORD PTR [rsi-80]
    movdqa  xmm3, XMMWORD PTR [rsi-64]
    movdqa  xmm2, XMMWORD PTR [rsi-48]
    movdqa  xmm1, XMMWORD PTR [rsi-32]
    movdqa  xmm0, XMMWORD PTR [rsi-16]
    movdqa  xmm7, XMMWORD PTR [rsi-128]
    movntdq XMMWORD PTR [rdi-112], xmm6
    movntdq XMMWORD PTR [rdi-96], xmm5
    movntdq XMMWORD PTR [rdi-80], xmm4
    movntdq XMMWORD PTR [rdi-64], xmm3
    movntdq XMMWORD PTR [rdi-48], xmm2
    movntdq XMMWORD PTR [rdi-128], xmm7
    movntdq XMMWORD PTR [rdi-32], xmm1
    movntdq XMMWORD PTR [rdi-16], xmm0
    sub eax, 1
    jne .L5
.L1:
    rep ret

暂时把这个答案留在这里,尽管现在很明显 OP 只想要一个 16B 传输。在 Linux,他的代码导致通过 PCIe 总线进行两次 8B 传输。

为了写入 MMIO space,值得尝试 movnti 写入组合存储指令。 movnti 的源操作数是 GP 寄存器,而不是向量寄存器。

如果您在驱动程序代码中 #include <immintrin.h>,您可能可以使用内在函数生成它。这在内核中应该没问题,只要您注意使用的内在函数。它没有定义任何全局变量。


所以这部分的大部分内容都不是很相关。

在大多数 CPU 上(rep movs 是好的),Linux's memcpy uses it。它只使用回退到 CPU 的显式循环,其中 rep movsqrep movsb 不是好的选择。

当大小是编译时间常数时,memcpy has an inline implementation 使用 rep movslrep movsd 的 AT&T 语法),然后进行清理:非rep movswmovsb 如果需要的话。 (实际上有点笨拙,IMO,因为大小 一个 compile-time 常量。也没有利用快速 rep movsb CPU 有。)

英特尔 CPU 自 P6 以来至少有相当不错的 rep movs 实现。参见

但是,关于 memcpy 仅在 64 位块中移动的说法你仍然是错误的,除非我误读了代码或者你在一个它决定使用回退循环的平台上。

无论如何,我不认为你使用正常的 Linux memcpy 会错过很多性能,除非你实际上单步执行了你的代码看到它做了一些傻事.

对于大副本,无论如何您都需要设置 DMA。 CPU 驱动程序的使用情况很重要,而不仅仅是在空闲系统上可以获得的最大吞吐量。 (小心不要过于相信微基准测试。)


在内核中使用 SSE 意味着 saving/restoring 向量寄存器。 RAID5/RAID6 代码是值得的。该代码只能 运行 来自专用线程,而不是来自 vector/FPU 寄存器仍然有另一个进程数据的上下文。

Linux 的 memcpy 可以在任何上下文中使用,因此它避免使用通常的整数寄存器以外的任何东西。我确实找到了 an article about an SSE kernel memcpy patch,其中 Andi Kleen 和 Ingo Molnar 都说总是将 SSE 用于 memcpy 并不好。也许可能会有一个特殊的 bulk-memcpy 用于大副本,值得保存向量 regs。

可以在内核中使用SSE,but you have to wrap it in kernel_fpu_begin() and kernel_fpu_end(). On Linux 3.7 and later, kernel_fpu_end() actually does the work of restoring FPU state,所以不要在一个函数中使用很多fpu_begin/fpu_end对。另请注意,kernel_fpu_begin 禁用抢占,您不得 "do anything that might fault or sleep".

理论上,只保存一个向量 reg,例如 xmm0 就可以了。您必须确保使用 SSE, 而不是 AVX 指令,因为您需要避免将 ymm0 / zmm0 的上半部分归零。当您 return 对使用 ymm regs 的代码进行编码时,您可能会导致 AVX+SSE 停顿。除非您想完整保存矢量 regs,否则您不能 运行 vzeroupper。即使要做到这一点,您也需要检测 AVX 支持...

但是,即使是这个 one-reg save/restore 也需要您采取与 kernel_fpu_begin 相同的预防措施,并禁用抢占。由于您将存储到自己的私人保存槽(可能在堆栈上),而不是 task_struct.thread.fpu,我不确定即使禁用抢占也足以保证 user-space FPU 状态不会被破坏。也许是,但也许不是,我不是内核黑客。禁用中断来防止这种情况也可能比仅使用 kernel_fpu_begin()/kernel_fpu_end() 触发完整的 FPU 状态保存更糟糕,使用 XSAVE/XRSTOR.