我不认为有任何非常有效的方法来处理不同元素具有不同值的动态序列。如果您不能利用与先前元素的相似性,64 个不同的字节值是相当高的熵。
从内存广播 4 字节或 8 字节模式(从 mov-immediate 到整数寄存器)或 16 字节模式很容易。或者以 vpmovzxbd 为例,使用更宽的元素(word、dword 或 qword)“压缩”shuffle 常量的存储,以加载时额外的 shuffle uop 为代价。或者generate something on the fly,其中每个元素都具有相同的值,从全一字节的向量开始。但是除非你手动编写 asm,否则编译器将通过内在函数不断传播,所以你任由他们摆布。其中一些足够聪明,可以使用广播负载而不是将 _mm512_set1_epi32(0x03020100) 扩展为 64 字节,但并非总是如此。
没有对每个元素执行不同操作的指令,并且乘法技巧仅限于 64 位块的宽度。
0x01010101 squared 的有趣技巧,这可能是一个很好的起点,除非您也可以直接从 mov eax, 0x00010203 / vpbroadcastd xmm0, eax(或 ZMM)或 vmovd xmm0, eax,或 64 位 mov rax, 0x0001020304050607(10 字节)开始) / vpbroadcastq zmm0, rax(6 字节)比vternlogd zmm0,zmm0,zmm0, -1 / vpabsb zmm0, zmm0(得到set1_epi8(1))加上vpmullq zmm0,zmm0,zmm0 / vpsllq zmm0, zmm0, 8便宜。
甚至没有扩大 64 位 => 128 位乘法,尽管 AVX-512 确实有 vpmullq 而 AVX2 没有。然而,它在 Intel CPU 上是 2 微指令。 (一个在 Zen4 上)。
每个 AVX-512 指令至少有 6 个字节(4 字节 EVEX + 操作码 + modrm),因此如果您针对 .text+.rodata 的纯大小进行优化(这在循环外可能并非不合理),那么加起来很快。您仍然不希望一个实际循环在 16 次迭代中一次存储 4 个字节,例如 add eax, 0x04040404 / stosd,即使在循环之外,它也会比您想要的慢。
以 set1_epi32(0x03020100) 或 64 位或 128 位版本开始,仍然需要多次洗牌并添加步骤以扩大到 512 位,并向广播结果的每个部分添加适量的 0x04、0x08 或 0x10 .
我想不出更好的东西,它仍然不够好用。与 ZMM 相比,使用一些 AVX2 指令可以一直节省代码大小,除非我缺少一种保存指令的方法。
该策略是在 ZMM 中创建 [ 0x30 repeating | 0x20 repeating | 0x10 repeating | 0x00 repeating] 并将其添加到广播 16 字节模式中。
default rel
vpbroadcastd ymm1, [vec4_0x10] ; we're loading another constant anyway, this is cheaper
vpaddd ymm2, ymm1,ymm1 ; set1(0x20)
vmovdqa xmm3, xmm1 ; [ set1(0) , set1(0x10) ] ; mov-elimination
vpaddd ymm4, ymm3, ymm2 ; [ set1(0x20), set1(0x30) ]
vshufi32x4 zmm4, zmm3, zmm4, 0b00_01_00_01 ; _MM_SHUFFLE(0,1,0,1) works like shufps but in 16-byte chunks.
vbroadcasti64x2 zmm0, [vec16_0to15]
vpaddb zmm0, zmm0, zmm4 ; memory-source broadcast only available with element size, e.g. vpaddq z,z,m64{1to8} but that'd take more granular shuffling
section .rodata
align 16
vec16_0to15: db 0,1,2,3,4,5,6,7
db 8,9,10,11,12,13,14,15
vec4_0x10: dd 0x10101010
大小:机器代码:0x2c 字节。常量:16 + 4 = 0x14。
总计:0x40 = 64 字节,与将整个文字常量放入内存相同。
屏蔽可能会节省向量指令,但代价是需要设置屏蔽寄存器值,成本为mov eax, imm32 / kmov k1, eax。
所以这节省了大约 9 个字节,ZMM 加载的大小,使用 RIP 相对寻址模式将其从 .rodata 放入寄存器。或 4 个字节,RIP 相对寻址模式的大小,vpaddb zmm0, zmm0, zmm31 与 vpaddb zmm0, zmm0, [vector_const] 之间的差异,具体取决于您使用它做什么。
$ objdump -drwC -Mintel foo
0000000000401000 <_start>:
401000: c4 e2 7d 58 0d 07 10 00 00 vpbroadcastd ymm1,DWORD PTR [rip+0x1007] # 402010 <vec4_0x10>
401009: c5 f5 fe d1 vpaddd ymm2,ymm1,ymm1
40100d: c5 f9 6f d9 vmovdqa xmm3,xmm1
401011: c5 e5 fe e2 vpaddd ymm4,ymm3,ymm2
401015: 62 f3 65 48 43 e4 11 vshufi32x4 zmm4,zmm3,zmm4,0x11
40101c: 62 f2 fd 48 5a 05 da 0f 00 00 vbroadcasti64x2 zmm0,XMMWORD PTR [rip+0xfda] # 402000 <vec16_0to15>
401026: 62 f1 7d 48 fc c4 vpaddb zmm0,zmm0,zmm4
$ size foo
text data bss dec hex filename
64 0 0 64 40 foo
我确实确认这适用于附加到 SDE 的 GDB:
# stopped before the last vpaddb
(gdb) p /x $zmm0.v64_int8
$2 = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0,
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}
(gdb) p /x $zmm4.v64_int8
$3 = {0x0 <repeats 16 times>, 0x10 <repeats 16 times>, 0x20 <repeats 16 times>, 0x30 <repeats 16 times>}
(gdb) si
0x000000000040102c in ?? ()
(gdb) p /x $zmm0.v64_int8
$4 = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f}