我正在linux中为PCIe设备编写设备驱动程序。此设备驱动程序执行多次读写操作以测试吞吐量。当我使用memcpy时,TLP的最大有效载荷是8个字节(在64位架构上)。在我看来,获得16字节有效载荷的唯一方法是使用SSE指令集。我已经看过this,但代码没有编译(AT& T / Intel语法问题)。
答案 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 movsq
或rep movsb
不是好选择的CPU,它只使用回退到显式循环。
当大小是编译时常量时,memcpy has an inline implementation使用rep movsl
(rep movsd
的AT&amp; T语法),然后进行清理:非 - rep
{如果需要,{1}}和movsw
。 (实际上有点笨重,IMO,因为 编译时常量 编译时常量。也没有利用快速movsb
的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使用的是非临时商店。我之前已经多次讨论过这个问题,例如here和here。我建议你先阅读这些内容然后再继续。
但是,如果你真的想在链接中生成内联汇编代码,你在这里提到的是你如何做到这一点:改为使用内在函数。
您无法使用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;
}
}
请注意,src
和dest
需要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