【问题标题】:How to load a avx-512 zmm register from a ioremap() address?如何从 ioremap() 地址加载 avx-512 zmm 寄存器?
【发布时间】:2020-03-16 03:15:58
【问题描述】:

我的目标是创建一个负载超过 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'`。

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

【问题讨论】:

  • 内核代码通常在禁用 SSE/AVX 的情况下编译,因此编译器永远不会生成涉及 xmm/ymm/zmm 寄存器的指令。不过,最好将此作为单个 asm 语句,因为通常这两个 asm 语句之间没有任何联系,除了两者都是 asm volatile 强制排序。为什么示例选择 ymm1?与 ymm0..7 的任何其他随机选择一样好。
  • 使用 zmm 加载时,请确保您的数据是 64 字节对齐的。考虑到内在函数的可用性,你确定你需要内联汇编吗? IA 有一些downsides
  • @DavidWohlferd:我最初评论了同样的事情(使用内在函数),但后来注意到这是内核代码。将 ZMM regs 用作私有线程本地存储的整个想法仅对 -mno-sse 有意义,因此编译器无法生成任何涉及向量 regs 的代码。不过,它必须在 kernel_fpu_being / end 之间运行。虽然我最初认为这是关于在一些 uarches 上进行 64 字节原子加载,但在一个 asm 块中进行加载+存储会更有意义,因此需要进行一些编辑。
  • 我绝对可以轻松地将两个 asm 合并为一个。我的主要问题更多关于zmm 注册部分。将 ymm 替换为 zmm 是不够的。
  • 等等什么?您说您想将矢量 regs 用作“类似内存的存储”。您是否真的只想将 64 字节复制到本地临时数组中,其中加载部分使用一条指令来加载整个缓存行? (因此在当前的 CPU 上可能是原子的。)您可以编辑您的问题以更多地说明您的用例吗? (另请注意,我在几分钟内编辑了我的第一条评论,但您似乎在回复我说“像普通人一样使用内在函数”的原始版本,例如 __m512i tmp = _mm512_load_si512(addr)

标签: gcc x86-64 inline-assembly avx avx512


【解决方案1】:

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


(3):内核代码在禁用 SSE/AVX 的情况下编译,因此编译器永远不会生成涉及 xmm/ymm/zmm 寄存器的指令。(对于大多数内核,例如 Linux)。这就是使该代码“安全”地避免在 asm 语句之间修改寄存器的原因。尽管 Linux md-raid 代码可以做到这一点,但为这个用例制作单独的语句仍然不是一个好主意。 OTOH 让编译器在存储和加载之间安排一些其他指令并不是一件坏事。

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

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

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

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


另请注意,使用 ZMM 寄存器会暂时降低该内核的最大涡轮时钟速度(或在“客户端”芯片上,所有内核的时钟速度被锁定在一起)。见SIMD instructions lowering CPU frequency

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

借助快速 L1d 缓存和存储转发,您确定将 ZMM 寄存器用作快速“类似内存”(线程本地)存储会有所收获吗?尤其是当您只能从 SIMD 寄存器中获取数据并通过从数组中存储/重新加载(或更多内联 asm 来洗牌......)返回整数 regs 时。 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?与 ymm0..7 的任何其他随机选择一样好,以允许 2 字节 VEX 前缀(ymm8..15 在这些指令上可能需要更多代码大小。)禁用 AVX 代码生成后,无法要求编译器选择一个方便的寄存器,带有一个虚拟输出操作数。

uint8_t datareg[32]; 坏了;它必须是 alignas(32) uint8_t datareg[32]; 以确保 vmovdqa 存储不会出错。

输出上的"memory"clobber 没用;整个数组已经是一个输出操作数,因为您将数组变量命名为输出,而不仅仅是一个指针。 (事实上​​,转换为指向数组的指针是告诉编译器一个普通的解引用指针输入或输出实际上更宽的方式,例如,对于包含循环的 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 将每个元素屏蔽作为任何指令的一部分,包括加载/存储。 这意味着有 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 explorergcc -O3 -mno-sse 上编译到

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

不知道你的__force是怎么定义的;它可能会出现在addr 的前面,而不是作为数组指针类型。或者它可能是volatile const char 数组元素类型的一部分。同样,请参阅 How can I indicate that the memory *pointed* to by an inline ASM argument may be used? 了解有关该输入转换的更多信息。

由于你正在读取 IO 内存,asm volatile 是必要的;对同一地址的另一次读取可能会读取不同的值。如果您正在读取另一个 CPU 内核可能已异步修改的内存,则同样如此。

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


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

如果您需要订购。其他非volatile 内存访问,您可以使用"memory" clobber。但是asm volatile 是订购的。 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 movntdqa 也应该可以工作。或 2x vmovntdqa ymm 加载;我建议这样做以避免涡轮增压。

【讨论】:

  • 很抱歉评论晚了,还有很多事情要做和学习。我以你的回答作为阅读的基础,我正在慢慢学习所有的基础知识。
  • 深入研究 WC 功能后,我的设备需要支持任何大小的 TLP,我在英特尔的论坛中读到,没有任何迹象表明生成的 TLP 来自 WC 缓冲区。解决方案将保留此处定义的解决方案,使用这些指令背靠背发送几个 16/32/64B TLP。我一直在学习。
  • @Alexis_FR_JP:不幸的是,我没有任何调整 PCIe 驱动程序以生成全尺寸事务的经验。我不知道各种做事方式实际上会发生什么。
  • 当然,我只是想提供一个更新。再次感谢您的回答(以及其他非常有用的不同问题!)
  • @Alexis_FR_JP:好的,很酷。如果您发现任何有用的补充,请随时发布您自己对此问题的答案。例如如果有一种好方法可以在没有 SIMD 向量的情况下获得广泛的 PCIe 读取事务,那将很有用。比如rep movs
猜你喜欢
  • 1970-01-01
  • 2016-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多