memcpy在linux中移动128位

我正在为Linux设备编写一个设备驱动程序。 该设备驱动程序执行多个读写操作来testing吞吐量。 当我使用memcpy时, TLP的最大有效载荷是8字节(在64位体系结构上)。 在我看来,获得16字节有效载荷的唯一方法是使用SSE指令集。 我已经看到这个,但代码不能编译(AT&T / Intel语法问题)。

  • 有一种方法可以使用Linux内的代码?
  • 有谁知道在哪里可以find一个移动128位的memcpy的实现?

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

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

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

第四,如果可能的话,你应该避免使用内联汇编器,因为编译器不可能安排通过asm语句的指令(至少这是真的)。 相反,你可以使用像vector_size属性一样的GCC扩展:

 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内核中使用特殊的寄存器,你应该采取预防措施,在上下文切换期间有一些寄存器不能被保存。 上证所注册是这些的一部分。

第七,当你使用这个来测试吞吐量时,你应该考虑处理器是否是一个重要的瓶颈。 比较代码的实际执行与从RAM写入/写入(是否打到或未命中缓存?)或从外部读取/写入。

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

现在离开这个答案,即使现在很清楚,OP只是想要一个16B传输。 在Linux上,他的代码通过PCIe总线导致两个8B传输。

为了写入MMIO空间,值得使用movnti write-combining-store指令。 movnti的源操作数是GP寄存器,而不是矢量寄存器。

如果在驱动程序代码中包含#include <immintrin.h> ,则可以使用内在函数生成该函数。 在内核中这应该没问题,只要你小心使用什么内在函数。 它没有定义任何全局变量。


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

在大多数CPU上( rep movs是好的), Linux的memcpy使用它 。 它只使用一个回rep movsq显式循环的CPU,其中rep movsqrep movsb不是好的选择。

当size是一个编译时常量时, memcpy具有一个使用rep movslrep movslrep movsl AT&T语法) 的内联实现 ,然后清理:non- rep movswmovsb如果需要)。 (其实有点笨重,国际海事组织,因为大小一个编译时间常数,也没有利用CPU的快速rep movsb有它。

自P6以来,英特尔CPU至少有相当不错的rep movs实现。 看安迪·格莱的评论 。

但是,对于仅在64位块中移动的memcpy,您还是错了,除非我误读了代码,或者您正在使用回退循环的平台。

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

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


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

Linux的memcpy可以在任何情况下使用,所以它可以避免使用通常的整数寄存器。 我找到了一篇关于SSE内核memcpy补丁的文章 ,Andi Kleen和Ingo Molnar都说,总是使用SSE作为memcpy是不好的。 也许可能会有一个特殊的批量memcpy的大副本,值得保存矢量注册表。

可以在内核中使用SSE, 但是您必须将其包装在kernel_fpu_begin()kernel_fpu_end() 。 在Linux 3.7和更高版本上, kernel_fpu_end()实际上是恢复FPU状态的工作 ,所以不要在函数中使用大量的fpu_begin / fpu_end对。 另请注意,kernel_fpu_begin禁用抢占,并且不能“做任何可能的错误或睡眠”。

理论上讲,只保存一个向量reg,比如xmm0就好。 你必须确保你使用了SSE 而不是 AVX指令,因为你需要避免调零ymm0 / zmm0的上半部分。 当您返回使用ymm regs的代码时,您可能会导致AVX + SSE失速。 除非你想做一个完整的矢量注册表,否则你不能运行vzeroupper。 甚至要做到这一点,你需要检测AVX支持…

但是,即使这样做了一次性保存/恢复,也需要您采取与kernel_fpu_begin相同的预防措施,并禁用抢占。 既然你将存储到你自己的专用存储槽(在堆栈中的概率),而不是task_struct.thread.fpu ,我不确定即使禁用抢占足以保证用户空间的FPU状态不会被破坏。 也许是,但也许不是,我不是内核黑客。 禁用中断也可能会比使用kernel_fpu_begin()/kernel_fpu_end()只使用kernel_fpu_begin()/kernel_fpu_end()触发完整的FPU状态保存更糟糕。

你提到的链接是使用非临时商店。 之前我已经讨论过这个,例如这里和这里 。 我建议你在进一步阅读之前阅读这些内容。

但是,如果您真的想在链接中提供内联汇编代码,您可以这样做:使用intrinsics代替。

不能用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])); } } 

为什么可以在这里找到更多的细节。


这里是使用GCC -O3 -S -masm=intel内在函数的X_aligned_memcpy_sse2函数中的程序集。 注意,它和这里基本相同。

  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