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

How to load a avx-512 zmm register from a ioremap() address?

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

对于 128b 和 256b,我可以分别使用 xmmymm 寄存器并且按预期工作。

现在,我想对 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编辑:在编译时检查是否支持 __AVX512F____AVX2__

  1. 为什么此示例使用 ymm1 而不是其他寄存器 ymm0-2-3-4..15
  2. 如何读取 512b zmm 寄存器的地址?
  3. 如何确保寄存器不会在两 asm 行之间被覆盖?

只需将 ymm 替换为 zmmgcc 显示 Error: operand size mismatch forvmovdqa'`。

如果该代码不正确或不是最佳实践,请先解决这个问题,因为我刚刚开始深入研究它。

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


(3): 内核代码在 SSE/AVX 禁用的情况下编译,因此编译器永远不会生成触及 xmm/ymm/zmm 寄存器的指令。(对于大多数内核,例如 Linux)。这就是使此代码 "safe" 在 asm 语句之间修改寄存器的原因。尽管 Linux md-raid 代码确实这样做了,但让它们针对这个用例单独声明仍然是一个坏主意。 OTOH 让编译器在存储和加载之间安排一些其他指令并不是一件坏事。

asm 语句之间的排序由它们提供,它们都是 volatile - 编译器无法将易失性操作与其他易失性操作一起重新排序,只能使用普通操作。

例如,在 Linux 中,只有在调用 kernel_fpu_begin()kernel_fpu_end() 之间使用 FP / SIMD 指令才是安全的(它们是slow: begin 当场保存整个 SIMD 状态,end 恢复它或至少将其标记为需要在 return 到用户-space 之前发生)。 如果你弄错了,你的代码将默默地破坏用户-space向量寄存器!!

This is to be used in a kernel module compiled with EXTRA_CFLAGS += -mavx2 -mavx512f to support AVX-512.

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


另请注意,完全使用 ZMM 寄存器会暂时降低该内核的最大涡轮时钟速度(或在 "client" 芯片上,对于所有内核,因为它们的时钟速度被锁在一起)。参见 SIMD instructions lowering CPU frequency

I'd like to use 512b zmm* registers as memory-like storage.

有了快速的 L1d 缓存和存储转发,你确定你甚至可以从使用 ZMM 寄存器作为快速 "memory like"(线程本地)存储中获得任何好处吗?特别是当您只能从 SIMD 寄存器中获取数据并通过数组中的 store/reload 返回整数寄存器时(或更多内联 asm 以随机播放......)。 Linux 中的一些地方(如 md RAID5/RAID6)使用 SIMD ALU 指令进行块异或或 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 代码生成后,无法要求编译器选择一个方便的寄存器,带有一个虚拟输出操作数。

uint8_t datareg[32];坏了;它需要 alignas(32) uint8_t datareg[32]; 以确保 vmovdqa 商店不会出错。

输出上的"memory" clobber 没有用;整个数组已经是一个输出操作数,因为您将一个数组变量命名为输出,而不仅仅是一个指针。 (事实上​​,转换为指向数组的指针是告诉编译器一个普通的解引用指针输入或输出实际上更宽的方式,例如对于包含 loops 的 asm 或者在这种情况下对于使用 SIMD 的 asm当我们无法将向量告诉编译器时。)

asm 语句是可变的,因此它不会被优化掉以重用相同的输出。 asm 语句触及的唯一 C 对象是数组对象,它是一个输出操作数,因此编译器已经知道该效果。


AVX512 版本:

AVX512 将每个元素屏蔽作为任何指令的一部分,包括 loads/stores。 这意味着有 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, %%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内存,所以asm volatile是必要的;同一地址的另一次读取可能会读取不同的值。如果您正在读取另一个 CPU 内核可能已异步修改的内存,则相同。

否则我认为 asm volatile 是没有必要的,如果你想让编译器优化做同样的副本。


"memory" clobber 也不是必需的:我们告诉编译器输入和输出的完整宽度,因此它可以全面了解正在发生的事情。

如果您需要订购 wrt。其他非 volatile 内存访问,您可以为此使用 "memory" 破坏器。但是 asm volatile 是按顺序订购的。 volatile 指针的解引用,包括 READ_ONCE 和 WRITE_ONCE,你应该将它们用于任何无锁线程间通信(假设这是 Linux 内核)。


ZMM16..31 不需要 vzeroupper 来避免性能问题,EVEX 总是固定长度。

我只将输出缓冲区对齐了 16 个字节。如果有一个实际的函数调用没有为每个 64 字节加载进行内联,则将 RSP 对齐 64 的开销可能会超过 3/4 时间的缓存行拆分存储的成本。存储转发我认为在 Skylake-X 系列 CPUs.

上,从那个宽的存储到缩小该缓冲区块的重新加载仍然有效

如果您要读入更大的缓冲区,请将其用于输出,而不是通过 64 字节的 tmp 数组弹跳。


可能还有其他方法可以生成更宽的 PCIe 读取事务;如果内存位于 WC 区域,那么从同一个对齐的 64 字节块加载 4x movntdqa 也应该有效。或 2x vmovntdqa ymm 负载;我建议这样做以避免涡轮惩罚。