您需要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 将每个元素屏蔽作为任何指令的一部分,包括加载/存储。 这意味着有 vmovdqa32 和 vmovdqa64 用于不同的屏蔽粒度。(如果包含 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 数组元素类型的一部分。同样,请参阅 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 加载;我建议这样做以避免涡轮增压。