【问题标题】:memcpy moving 128 bit in linuxmemcpy 在 linux 中移动 128 位
【发布时间】:2016-03-04 08:22:03
【问题描述】:

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

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

【问题讨论】:

  • MMX 只有 64 位(字节)。 ITYM SSE,即 128 位(16 字节)。
  • 我对这个话题不是很了解,但你不能为此使用 DMA 吗?
  • @haster8558:“不起作用”没有任何意义。无论如何,除非你只编译你的代码(而不是使用任何包含asm 块的头文件),否则你应该在 asm 块的开头使用.intel_syntax noprefix,在结尾使用.att_syntax noprefix
  • 你应该调查一下Linux内核是否有它自己可以使用的快速memcpy函数。它甚至可以调整到它在启动时运行的特定 CPU。

标签: c linux assembly sse simd


【解决方案1】:

首先,您可能使用 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% 的速度运行)。

【讨论】:

  • 如果不采取额外的预防措施,您将无法在 Linux 内核中使用 SSE。除非必须,否则不会保存/恢复矢量 reg,因为正常的内核代码不会触及它们。
  • @PeterCordes 我并没有考虑太多这是在内核空间中,我已经更新了答案以指出这一点。
  • 只是一些澄清。问题不在于标准 memcpy 的速度,问题在于标准 memcpy 不允许在 PCIe 总线上传输 16 字节。瓶颈是 CPU,我很确定这一点,因为我使用的是协议分析器。在 Windows 中,在相同的硬件上,我可以看到 16 字节的有效负载,而在 linux 中我看不到。我得到的唯一想法是 SSE 指令集。
  • @haster8558 是否排除了RAM可能是瓶颈的可能性?
  • 没有证据表明 RAM 可能参与传输。那么 PCIe X1 的写入吞吐量在 Linux 上几乎是 70 MB/s,在 Windows 上几乎翻了一番,硬件也是一样的。在相同的硬件上,我想要相同的行为。瓶颈确实是 CPU。
【解决方案2】:

您提到的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

【讨论】:

  • 非常感谢。只是一个疑问,我的一位同事告诉我内在函数不能在内核模式内使用。我是不是误会了?
  • @haster8558,我不知道。你的同事为什么这么说?我唯一能想象的是编译器将内在转换为与内核不兼容的指令。您必须查看生成的组件。我想这是内在函数的一个缺点,因为您不能保证使用不同的编译器甚至不同版本的编译器得到完全相同的代码。
  • 问题是xmmintrin.h 包含stdlib.h 并且在编译过程中有几个问题。
  • 可以看到在 AT&T 语法中反汇编的 X_aligned_memcpy_sse2 吗?
  • @haster8558,是的gcc -O3 -S foo.c。再次注意,我不建议使用此代码。我只是想说明如何使用内在函数来做到这一点。
【解决方案3】:

暂时把这个答案留在这里,尽管现在很清楚 OP 只想要一个 single 16B 传输。在 Linux 上,他的代码导致了 PCIe 总线上的两次 8B 传输。

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

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


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

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

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

自 P6 以来的 Intel CPU 至少有相当好的rep movs 实现。见Andy Glew's comments on it

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

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

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


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

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

可以在内核中使用 SSE,but you have to wrap it in kernel_fpu_begin() and kernel_fpu_end()。在 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 停止。除非您想完全保存向量 reg,否则无法运行 vzeroupper。即使这样做,您也需要检测 AVX 支持...

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

【讨论】:

  • 谢谢,我试着解释得更好。我有一个通过 PCIe 总线连接到我的 CPU 的 FPGA。在 CPU 和 FPGA 之间有一个 PCIe 协议分析器。我在 Windows 中看到,当(在设备驱动程序中)我调用块大小大于 16 字节的 memcpy 时,内存写入的有效负载为 16 字节。在linux中我看不到同样的东西。在 Windows 中,我只能在使用 WDF 构建的设备驱动程序中看到这种行为。关于 memcpy 的速度我不在乎,因为 CPU 需要相同的时间为 roocomplex 生成 TLP(具有 8 或 16 个有效负载),所以没有任何区别。
  • 当然,DMA 传输是大数据块的解决方案,但我希望能够一键传输 16 字节。必须对 DMA 进行编程。意思是向端点发送至少2个TLP并等待中断。
  • @haster8558:对,对于仅传输 16B 的数据,显然您不想对它进行 DMA。您的问题方式遗漏了太多细节,任何人都无法弄清楚您在做什么。每个人都认为您在谈论批量传输,而不是单个 128b。我自己不编写设备驱动程序,所以我什至不知道 TLP 是什么。我不确定当您 rep movsq 往返 MMIO 空间时会发生什么,而不是在两个正常的写回内存区域之间。但听起来你得到了 64 位块:/
  • 有兴趣看看一些实验证据。我测试了 glibc 的 memcpy 和 gcc 的内置 memcpy,发现在某些情况下很容易击败它们。我基本上重做了 Agner Fog 在他的优化 C++ 手册中所做的测试。如果 Linux 的 memcpy 更好,我会感到惊讶。我愿意打赌,使用非临时存储和多线程的大小比最后一级缓存大得多,这将击败 Linux 的 memcpy。
  • @haster8558:我的意思是机器代码,而不是高级源代码......我想知道 Windows 是否有一个没有 SSE 的技巧,或者 Windows 驱动程序是否只使用 SSE。我从未编写过 Windows 内核/驱动程序代码,对此一无所知。尝试反汇编windows驱动中的相关功能。
猜你喜欢
  • 2012-01-30
  • 1970-01-01
  • 1970-01-01
  • 2010-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多