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 语法问题)。
- 有一种方法可以在 linux 中使用该代码吗?
- 有谁知道我在哪里可以找到移动 128 位的 memcpy 的实现?
首先,您可能使用 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;
}
}
注意 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]));
}
}
有关原因的更多详细信息,请参见 。
这是 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 movsq
或 rep movsb
不是好的选择。
当大小是编译时间常数时,memcpy has an inline implementation 使用 rep movsl
(rep movsd
的 AT&T 语法),然后进行清理:非rep
movsw
和 movsb
如果需要的话。 (实际上有点笨拙,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.
我正在 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 语法问题)。
- 有一种方法可以在 linux 中使用该代码吗?
- 有谁知道我在哪里可以找到移动 128 位的 memcpy 的实现?
首先,您可能使用 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;
}
}
注意 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]));
}
}
有关原因的更多详细信息,请参见
这是 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 movsq
或 rep movsb
不是好的选择。
当大小是编译时间常数时,memcpy has an inline implementation 使用 rep movsl
(rep movsd
的 AT&T 语法),然后进行清理:非rep
movsw
和 movsb
如果需要的话。 (实际上有点笨拙,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.