如何从ioremap()地址加载avx-512 zmm寄存器



我的目标是创建一个负载超过64b的PCIe事务。为此,我需要读取一个ioremap()地址。

对于128b和256b,我可以分别使用CCD_ 2和CCD_。

现在,我想对512bzmm寄存器(类似内存的存储?!)做同样的操作

许可证下的代码我不允许在这里显示,使用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-512edit:在编译时检查是否支持__AVX512F____AVX2__

  1. 为什么这个例子使用ymm1而不是不同的寄存器ymm0-2-3-4..15
  2. 如何读取512bzmm寄存器的地址
  3. 如何确保寄存器不会在两条asm线之间被覆盖

简单地用zmm替换ymmgcc显示Error: operand size mismatch forvmovdqa'`。

如果该代码不正确或是最佳实践,那么让我们先解决它,因为我刚刚开始深入研究它。

您需要vmovdqa32,因为AVX512具有每个元素的掩码;所有指令都需要SIMD元素大小。请参阅下面的内容以获取应该安全的版本。如果你阅读vmovdqa的手册,你就会看到这一点;ZMM的vmovdqa32记录在同一条目中。


(3):内核代码是在禁用SSE/AVX的情况下编译的,因此编译器永远不会生成触及xmm/ymm/zmm寄存器的指令(适用于大多数内核,例如Linux)。这就是为什么此代码"安全",不会在asm语句之间修改寄存器。尽管Linux md raid代码可以做到这一点,但在这个用例中,让它们单独声明仍然是个坏主意。OTOH让编译器在存储和加载之间调度一些其他指令并不是一件坏事。

asm语句之间的排序是由它们都是volatile提供的——编译器不能将volatile操作与其他volatile运算重新排序,只能使用普通运算。

例如,在Linux中,只有在对xmm0和kernel_fpu_end()的调用之间使用FP/SIMD指令才是安全的(这很慢:begin当场保存整个SIMD状态,end在返回用户空间之前将其恢复或至少标记为需要发生)如果你弄错了,你的代码会悄悄地破坏用户空间矢量寄存器

这将用于使用EXTRA_CFLAGS+=-mavx2-mavx512f编译的内核模块中,以支持AVX-512。

您不能这样做让编译器在内核代码中发出自己的AVX/AVX512指令可能是灾难性的,因为你无法阻止它在kernel_fpu_begin()之前破坏向量reg。仅通过内联asm使用矢量regs。


还要注意,使用ZMM寄存器会暂时降低该内核的最大turbo时钟速度(或在"客户端"芯片上,因为所有内核的时钟速度都锁定在一起)。请参阅降低CPU频率的SIMD指令

我想使用512bzmm*寄存器作为类似内存的存储。

有了快速的L1d缓存和存储转发,你确定使用ZMM寄存器作为快速的"类内存"(线程本地)存储会有什么收获吗?尤其是当你只能通过从数组中存储/重新加载(或更多的内联asm来shuffle…)将数据从SIMD寄存器中取出并返回到整数寄存器时。Linux中的一些地方(如mdRAID5/RAID6)使用SIMD ALU指令进行块XOR或RAID6奇偶校验,这值得kernel_fpu_begin()的开销。但是,如果你只是加载/存储以使用ZMM/YMM状态作为存储,不能缓存未命中,不能在大缓冲区上循环,这可能不值得。

(编辑:事实证明,您实际上想要使用64字节副本来生成PCIe事务,这与将数据长期保存在寄存器中完全不同。)


如果您只想用一条指令加载复制64个字节

就像你实际做的那样,获得一个64字节的PCIe事务。

最好将其作为一个asm语句,因为否则这两个asm声明之间就没有任何联系,除非两者都是asm volatile强制执行该排序。(如果您在启用AVX指令以供编译器使用的情况下执行此操作,那么您只需使用内部函数,而不是"=x"/"x"输出/输入来连接单独的asm语句。)

示例为什么选择ymm1?与允许2字节VEX前缀的ymm0..7的任何其他随机选择一样好(ymm8..15可能需要这些指令上更多的代码大小。)禁用AVX代码生成后,无法要求编译器为您选择一个带有伪输出操作数的方便寄存器。

CCD_ 28坏掉;它需要是CCD_ 29以确保CCD_。

ymm1对输出的撞击是无用的;整个数组已经是一个输出操作数,因为您将数组变量命名为输出,而不仅仅是一个指针。(事实上,强制转换为指针到数组是告诉编译器,纯去引用指针输入或输出实际上更宽的方式,例如,对于包含循环的asm,或者在这种情况下,对于使用SIMD的asm(当我们无法告诉编译器向量时)。我如何指示可以使用内联asm参数指向的内存*?)

asm语句是不稳定的,因此不会对其进行优化以重用相同的输出。asm语句唯一接触的C对象是数组对象,它是一个输出操作数,因此编译器已经知道这种影响。


AVX512版本:

AVX512将每个元素屏蔽作为任何指令的一部分,包括加载/存储这意味着对于不同的掩蔽粒度存在vmovdqa32vmovdqa64(如果包含AVX512BW,则为vmovdqu8/16/32/64)。FP版本的指令已经在助记符中加入了ps或pd,因此对于那里的ZMM向量,助记符保持不变。如果您查看编译器为具有512位向量或内部函数的自动向量化循环生成的asm,您会立即看到这一点。

这应该是安全的:

#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, %%zmm16nt"   // 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;
}

gcc -O3 -mno-sse到在Godbolt编译器资源管理器上编译

foo:
vmovdqa32  (%rdi), %zmm16
vmovdqu32  %zmm16, -72(%rsp)
movl    -64(%rsp), %eax
ret

我不知道你的__force是怎么定义的;它可能在CCD_ 38前面而不是作为数组指针类型。或者它可能是volatile const char数组元素类型的一部分。请再次参阅如何指示可以使用内联ASM参数指向的内存*?了解有关该输入铸件的更多信息。

由于您正在读取IO内存,asm volatile是必需的;对相同地址的另一次读取可以读取不同的值。如果您正在读取另一个CPU内核可能异步修改的内存,也是如此。

否则,我认为asm volatile是不必要的,如果你想让编译器优化去做同样的复制。


"memory"clobber也是不必要的:我们告诉编译器输入和输出的全宽,这样它就可以全面了解正在发生的事情。

如果您需要对其他非volatile内存访问进行排序,则可以使用"memory"clobber。但是asm volatile是根据volatile指针的引用排序的,包括READ_ONCE和WRITE_ONCE,您应该将它们用于任何无锁线程间通信(假设这是Linux内核)。


ZM16..31不需要vzerooper来避免性能问题,EVEX始终是固定长度。

我只将输出缓冲区对齐了16个字节。如果有一个实际的函数调用没有为每个64字节的加载内联,那么用64对齐RSP的开销可能会超过缓存线分割存储3/4的时间。我认为,从Skylake-X系列CPU上的宽存储到窄缓冲区块的重新加载,存储转发仍然有效。

如果要读取更大的缓冲区,请将其用于输出,而不是在64字节的tmp数组中跳跃


可能还有其他方法可以生成更宽的PCIe读取事务;如果存储器在WC区域中,那么来自相同对齐的64字节块的4xmovntdqa加载也应该工作。或2个vmovntdqa ymm负载;我建议这样做以避免涡轮增压处罚。

最新更新