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

问题描述 投票:0回答:1

我的目标是创建一个具有超过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-512kernel模块中使用。

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

ymm简单地替换zmmgcc显示Error: operand size mismatch for vmovdqa'。

如果该代码不正确或不是最佳实践,那么从我刚开始研究该代码开始,首先解决它。

gcc x86-64 inline-assembly avx avx512
1个回答
0
投票

您需要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状态,然后结束将其恢复,或者至少将其标记为需要在返回用户空间之前进行)。 如果您弄错了,您的代码将悄无声息地破坏用户空间矢量寄存器!

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

[您不能这样做。

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

[还要注意,完全使用ZMM寄存器会暂时降低该内核的最大Turbo时钟速度]

(或在“客户端”芯片上,对于所有内核,因为它们的时钟速度被锁定在一起)。参见SIMD instructions lowering CPU frequency

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

使用快速的L1d缓存和存储转发,您确定通过将ZMM寄存器用作快速的“类似内存”(线程本地)存储,甚至还能获得任何收益吗?尤其是当您只能从SIMD寄存器中获取数据并通过存储/从数组中重新加载(或者通过更多的内联asm进行改组...)将数据返回到整数reg中时。 Linux中的一些地方(例如md RAID5 / RAID6)使用SIMD ALU指令进行块XOR或raid6奇偶校验,因此值得kernel_fpu_begin()的开销。但是,如果您正在[[just

加载/存储以使用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"破坏符是无用的;整个数组已经是输出操作数,因为您将数组变量命名为输出,而不仅仅是指针。 (实际上,转换为指针到数组是您告诉编译器纯普通的解引用指针的输入或输出实际上更宽的方式,例如,对于包含循环的asm或在这种情况下,对于不能使用SIMD的asm,告诉编译器向量。How can I indicate that the memory *pointed* to by an inline ASM argument may be used?

asm语句是易失性的,因此不会对其进行优化以重用相同的输出。 asm语句涉及的唯一C对象是数组对象,它是输出操作数,因此编译器已经知道这种效果。


AVX512版本:

AVX512在任何指令(包括加载/存储)中都具有逐元素的掩码。

这意味着vmovdqa32vmovdqa32具有不同的屏蔽粒度。

(如果包含AVX512BW,则为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

内核)。
ZMM16..31不需要vzeroupper来避免性能问题,并且EVEX始终是固定长度。

我仅将输出缓冲区对齐16个字节。如果有一个实际的函数调用未针对每个64字节负载进行内联,则将RSP对齐64的开销可能会超过缓存行拆分存储的3/4的时间。我认为存储转发仍然可以从较大的存储区有效地工作,以缩小Skylake-X系列CPU上该缓冲区的重载范围。

如果要读入更大的缓冲区,请使用该缓冲区作为输出,而不是通过64字节的tmp数组弹跳。


可能还有其他方法来生成更广泛的PCIe读取事务

;如果存储器位于WC区域,则从同一对齐的64字节块进行的4x volatile加载也应起作用。或2x movntdqa负载;我建议您避免加速处罚。
© www.soinside.com 2019 - 2024. All rights reserved.