我的目标是创建一个具有超过64b有效负载的PCIe事务。为此,我需要读取一个ioremap()
地址。
对于128b和256b,我可以分别使用xmm
和ymm
寄存器,它们可以按预期工作。
现在,我想对512b zmm
寄存器(类似内存的存储?!)进行相同的操作
我未经许可在此处显示的授权代码,使用256b的汇编代码:
void __iomem *addr;
uint8_t datareg[32];
[...]
// Read memory address to ymm (to have 256b at once):
asm volatile("vmovdqa %0,%%ymm1" : : "m"(*(volatile uint8_t * __force) addr));
// Copy ymm data to stack data: (to be able to use that in a gcc handled code)
asm volatile("vmovdqa %%ymm1,%0" :"=m"(datareg): :"memory");
这将在用EXTRA_CFLAGS += -mavx2 -mavx512f
编译以支持AVX-512的kernel模块中使用。
ymm1
而不使用其他寄存器ymm0-2-3-4..15
?zmm
寄存器的地址?asm
之间的寄存器不会被覆盖?用ymm
简单地替换zmm
,gcc显示Error: operand size mismatch for
vmovdqa'。
如果该代码不正确或不是最佳实践,那么从我刚开始研究该代码开始,首先解决它。
您需要vmovdqa32
,因为AVX512具有逐元素遮罩;所有指令都需要SIMD元素大小。请参阅以下有关应该安全的版本。
((3):内核代码在禁用SSE / AVX的情况下进行编译,因此编译器将永远不会生成触摸xmm / ymm / zmm寄存器的指令。(对于大多数内核,例如Linux)。这就是使此代码“安全”避免在asm语句之间修改寄存器的原因。尽管针对Linux的md-raid代码可以做到这一点,但是仍然要针对此用例使用单独的语句仍然是一个坏主意。 OTOH让编译器在存储和加载之间安排一些其他指令不是一件坏事。
asm
语句之间的排序由它们都为volatile
提供-编译器不能仅使用普通操作对其他易失性操作重新排序易失性操作。
例如,在Linux中,在对kernel_fpu_begin()
和kernel_fpu_end()
的调用之间使用FP / SIMD指令是唯一安全的>>(这很慢:开始时立即保存整个SIMD状态,然后结束将其恢复,或者至少将其标记为需要在返回用户空间之前进行)。 如果您弄错了,您的代码将悄无声息地破坏用户空间矢量寄存器!
让编译器在内核代码中发出自己的AVX / AVX512指令可能是灾难性的,因为您无法阻止它在这将在用EXTRA_CFLAGS + = -mavx2 -mavx512f编译的内核模块中使用,以支持AVX-512。
[您不能这样做。
kernel_fpu_begin()
之前丢弃向量reg。仅通过内联汇编使用矢量reg。[还要注意,完全使用ZMM寄存器会暂时降低该内核的最大Turbo时钟速度]
(或在“客户端”芯片上,对于所有内核,因为它们的时钟速度被锁定在一起)。参见SIMD instructions lowering CPU frequency加载/存储以使用ZMM / YMM状态作为无法缓存丢失,不循环大缓冲区的存储,则可能不值得。((编辑:事实证明,您实际上想使用64字节副本来生成PCIe事务,这是一个完全独立的用例,而不是将数据长期保存在寄存器中。)我想使用512b zmm *寄存器作为类似内存的存储。
使用快速的L1d缓存和存储转发,您确定通过将ZMM寄存器用作快速的“类似内存”(线程本地)存储,甚至还能获得任何收益吗?尤其是当您只能从SIMD寄存器中获取数据并通过存储/从数组中重新加载(或者通过更多的内联asm进行改组...)将数据返回到整数reg中时。 Linux中的一些地方(例如
md
RAID5 / RAID6)使用SIMD ALU指令进行块XOR或raid6奇偶校验,因此值得kernel_fpu_begin()
的开销。但是,如果您正在[[just
最好将此作为单个asm语句,因为否则,两个asm语句之间不存在任何联系,除了两者都是asm volatile
会强制执行该顺序。 (如果在启用AVX指令供编译器使用的情况下执行此操作,则您将仅使用内部函数,而不是使用"=x"
/ "x"
输出/输入来连接单独的asm语句。)
为什么该示例选择ymm1?与允许使用2字节VEX前缀的ymm0..7的其他随机选择一样好(ymm8..15可能需要这些指令上更大的代码大小。)禁用AVX代码生成后,无法要求编译器选择使用伪输出操作数为您提供方便的寄存器。
uint8_t datareg[32];
损坏;必须为alignas(32) uint8_t datareg[32];
以确保vmovdqa
存储不会出错。
输出上的"memory"
破坏符是无用的;整个数组已经是输出操作数,因为您将数组变量命名为输出,而不仅仅是指针。 (实际上,转换为指针到数组是您告诉编译器纯普通的解引用指针的输入或输出实际上更宽的方式,例如,对于包含循环的asm或在这种情况下,对于不能使用SIMD的asm,告诉编译器向量。How can I indicate that the memory *pointed* to by an inline ASM argument may be used?)
asm
语句是易失性的,因此不会对其进行优化以重用相同的输出。 asm语句涉及的唯一C对象是数组对象,它是输出操作数,因此编译器已经知道这种效果。
这意味着vmovdqa32
和vmovdqa32
具有不同的屏蔽粒度。
vmovdqa64
)。指令的FP版本已经将ps或pd引入了助记符,因此该处的ZMM向量助记符保持不变。如果查看编译器生成的asm以查找具有512位向量或内在函数的自动向量化循环,就会立即看到此信息。这应该很安全:vmovdqu8/16/32/64
在#include <stdalign.h> #include <stdint.h> #include <string.h> #define __force int foo (void *addr) { alignas(16) uint8_t datareg[64]; // 16-byte alignment doesn't cost any extra code. // if you're only doing one load per function call // maybe not worth the couple extra instructions to align by 64 asm volatile ( "vmovdqa32 %1, %%zmm16\n\t" // aligned "vmovdqu32 %%zmm16, %0" // maybe unaligned; could increase latency but prob. doesn't hurt throughput much compared to an IO read. : "=m"(datareg) : "m" (*(volatile const char (* __force)[64]) addr) // the whole 64 bytes are an input : // "memory" not needed, except for ordering wrt. non-volatile accesses to other memory ); int retval; memcpy(&retval, datareg+8, 4); // memcpy can inline as long as the kernel doesn't use -fno-builtin // but IIRC Linux uses -fno-strict-aliasing so you could use cast to (int*) return retval; }
上用Godbolt compiler explorer编译为
gcc -O3 -mno-sse
我不知道您的foo: vmovdqa32 (%rdi), %zmm16 vmovdqu32 %zmm16, -72(%rsp) movl -64(%rsp), %eax ret
是如何定义的;它可能位于__force
的前面,而不是数组指针类型。或者,它可能是addr
数组元素类型的一部分。再次,请参见volatile const char
以获取有关该输入强制转换的更多信息。由于您正在读取IO内存,因此How can I indicate that the memory *pointed* to by an inline ASM argument may be used?是必需的;再次读取相同的地址可能会读取不同的值。如果您正在读取另一个CPU内核可能已异步修改的内存,则相同。
否则,如果您想让编译器优化执行相同的复制,则我认为asm volatile
是不必要的。
asm volatile
缓冲区也不是必需的:我们告诉编译器输入和输出的全宽,因此它可以全面了解正在发生的事情。如果需要订购wrt。其他非"memory"
内存访问,则可以使用volatile
缓冲区。但是"memory"
是按顺序排序的。 asm volatile
指针的取消引用,包括您应用于任何无锁线程间通信的READ_ONCE和WRITE_ONCE(假设这是
Linux
内核)。我仅将输出缓冲区对齐16个字节。如果有一个实际的函数调用未针对每个64字节负载进行内联,则将RSP对齐64的开销可能会超过缓存行拆分存储的3/4的时间。我认为存储转发仍然可以从较大的存储区有效地工作,以缩小Skylake-X系列CPU上该缓冲区的重载范围。
如果要读入更大的缓冲区,请使用该缓冲区作为输出,而不是通过64字节的tmp数组弹跳。
可能还有其他方法来生成更广泛的PCIe读取事务
;如果存储器位于WC区域,则从同一对齐的64字节块进行的4xvolatile
加载也应起作用。或2x movntdqa
负载;我建议您避免加速处罚。