memcpy在linux中移动128位

时间:2015-11-30 16:14:17

标签: c linux assembly sse simd

我正在linux中为PCIe设备编写设备驱动程序。此设备驱动程序执行多次读写操作以测试吞吐量。当我使用memcpy时,TLP的最大有效载荷是8个字节(在64位架构上)。在我看来,获得16字节有效载荷的唯一方法是使用SSE指令集。我已经看过this,但代码没有编译(AT& T / Intel语法问题)。

  • 有一种方法可以在linux中使用该代码吗?
  • 有谁知道我在哪里可以找到一个移动128位的memcpy的实现?

3 个答案:

答案 0 :(得分:7)

首先,您可能使用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的读/写(你是否命中缓存?)或从/写到外设的读取进行比较。

第八,移动数据时,应避免将大块数据从RAM移到RAM,如果是来自带宽有限的外设,则应该考虑使用DMA。请记住,如果它的访问时间限制了性能,CPU仍将被视为繁忙(尽管它无法以100%的速度运行)。

答案 1 :(得分:4)

暂时留下这个答案,即使现在已经很清楚,OP只想要一个 16B传输。在Linux上,他的代码通过PCIe总线导致两次8B传输。

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

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

因此,本节的大部分内容都不太相关。

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

当大小是编译时常量时,memcpy has an inline implementation使用rep movslrep movsd的AT&amp; T语法),然后进行清理:非 - rep {如果需要,{1}}和movsw。 (实际上有点笨重,IMO,因为 编译时常量 编译时常量。也没有利用快速movsb的CPU。 )

自P6以来,英特尔CPU至少具有相当不错的rep movsb实施。请参阅Andy Glew's comments on it

但是,你仍然错误地认为memcpy只能在64位块中移动,除非我误读了代码,或者你正处于决定使用回退循环的平台上。

无论如何,我不认为你使用普通的Linux rep movs错过了很多性能,除非你实际上单步执行代码并看到它做了一些愚蠢的事情

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

在内核中使用SSE意味着保存/恢复向量寄存器。对于RAID5 / RAID6代码来说是值得的。该代码可能只从专用线程运行,而不是从向量/ FPU寄存器仍有另一个进程数据的上下文运行。

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

可以在内核but you have to wrap it in kernel_fpu_begin() and kernel_fpu_end()中使用SSE。在Linux 3.7及更高版本,kernel_fpu_end() actually does the work of restoring FPU state,所以不要在函数中使用大量的fpu_begin / fpu_end对。另请注意,kernel_fpu_begin会禁用抢占,并且您不能“执行任何可能出错或休眠的”。

理论上,只保存一个向量reg,就像xmm0一样,会很好。您必须确保使用SSE, AVX指令,因为您需要避免将ymm0 / zmm0的上半部分归零。当您返回使用ymm regs的代码时,可能会导致AVX + SSE停顿。除非你想完全保存向量regs,否则你无法运行vzeroupper。即使这样做,您也需要检测AVX支持......

但是,即使执行此一次注册保存/恢复,也需要采取与memcpy相同的预防措施,并禁用抢先功能。由于你要存储到你自己的私有保存槽(堆栈上的概率),而不是kernel_fpu_begin,我不确定即使禁用抢占也足以保证用户空间FPU状态不会被破坏。也许是,但也许不是,而且我不是内核黑客。禁用中断以防止这种情况也可能比使用task_struct.thread.fpu使用XSAVE / XRSTOR触发完整FPU状态更糟糕。

答案 2 :(得分:3)

您提到的link使用的是非临时商店。我之前已经多次讨论过这个问题,例如herehere。我建议你先阅读这些内容然后再继续。

但是,如果你真的想在链接中生成内联汇编代码,你在这里提到的是你如何做到这一点:改为使用内在函数。

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

以下代码应在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]));
    }
}

有关为何可以找到here的详细信息。

以下是使用带有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