assembly 用递增字节填充AVX512寄存器

8oomwypt  于 2022-11-30  发布在  其他
关注(0)|答案(1)|浏览(149)

是否有一些不明显的技巧可以用递增字节(小端字节序)填充AVX 512寄存器?也就是说,等效于以下代码:

__m512i make_incrementing_bytes(void) {
    /* Compiler optimizes this into an initialized array in .rodata. */
    alignas(64) char data[sizeof(__m512i)];
    for (unsigned i = 0; i < sizeof(data); i++) {
        data[i] = i;
    }
    return _mm512_load_si512(data);
}

我看到的唯一明显的方法(以及愚者用上面的代码生成的方法)是从内存中使用vmovdqa64的通用方法--但是这个常数的熵足够低,看起来应该可以做得更好。
(我知道通常常量加载并不位于关键路径上,或者您有一个备用寄存器专门用于常量,以便能够重新加载它,但我很感兴趣这个指令集中是否隐藏了一些技巧。例如,对于一个具有全宽度寄存器乘法的指令集,您可以用0x 1填充每个字节,将寄存器平方,并将结果左移一位-但据我所知,这并不适合AVX 512。)

qrjkbowd

qrjkbowd1#

我不认为有任何非常有效的方法来动态地处理这样一个序列,其中不同的元素有不同的值。64个不同的字节值是相当高的熵,如果你不能利用与前面元素的相似性。
广播4字节或8字节模式很容易(从mov-immediate到整数寄存器),或者从内存中提取16或32字节模式。或者使用vpmovzxbd(例如),“压缩”具有更宽元素的混洗常量的存储(字、双字或四字),或者返回到generate something on the fly,其中从全1字节的向量开始,每个元素都具有相同的值。但是,除非你是手工编写asm,否则编译器会通过内部函数不断地传播,所以你只能任由它们摆布。有些编译器足够聪明,使用广播加载,而不是将_mm512_set1_epi32(0x03020100)扩展到64字节,但并不总是如此。
没有指令对每个元素做不同的事情,乘法技巧限于64位块的宽度。
0x01010101的平方是一个有趣的技巧,这可能是一个很好的起点,除了你可能直接从mov eax, 0x00010203/vpbroadcastd xmm0, eax开始(或ZMM)或vmovd xmm0, eax、或64位x1M6 N1 x(10字节)/x1M7 N1 x(6字节),其比x1M8 N1 x/x1M9 N1 x(以得到x1M10 N1 x)加上x1M11 N1 x/x1M12 N1 x便宜。
虽然AVX-512有vpmullq,而AVX 2没有,但它甚至没有扩展的64位=〉128位乘法。
每个AVX-512指令至少为6个字节(4字节EVEX + opcode + modrm),因此如果您针对纯.text+.rodata大小进行优化,则会快速增加(这在循环之外可能不是不合理的)。您仍然不希望实际的循环每次存储4个字节,进行16次迭代,如add eax, 0x04040404/stosd,即使在循环外也会比您希望的要慢。
set1_epi32(0x03020100)或64位或128位版本开始,仍然需要多个混洗和添加步骤来扩展到512位,并向广播结果的每个部分添加适量的0x 04、0x 08或0x 10。
我想不出更好的方法,而且它仍然不够好用。使用一些AVX 2指令比ZMM节省了代码长度,除非我缺少一种节省指令的方法。
策略是在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

尺寸:机器货号:0x 2c字节。常量:16 + 4 = 0x14。

**总计:0x 40 = 64字节,**等同于将整个文本常量放入内存。

屏蔽可能节省了向量指令,但代价是需要设置屏蔽寄存器值,这需要花费mov eax, imm32/kmov k1, eax
因此,这可以节省大约10个字节,相当于RIP相对寻址模式下ZMM从. rodata加载到寄存器的大小。或者,这可以节省4个字节,相当于RIP相对寻址模式下vpaddb zmm0, zmm0, zmm31vpaddb 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

我确认了这一点,并将GDB连接到SDE:

# 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}

我只是不知道一个简洁的方法来编写程序集中要加载的常量,既简洁又清楚
(例如,val: .rept 64 / .byte .-val / .endr满足前者,但不满足后者。)
这是GAS语法的一个巧妙的用法(当然,如果你想把所有内容放在一行上,;是语句分隔符)。
在NASM语法中,将%assign放在%rep 64中是很自然的方式,如NASM手册中使用%rep展开循环的示例所示。

align 64
vec64_0to63:        ; self-explanatory name for the constant points readers in the right direction
  %assign i 0 
  %rep    64 
    db  i
    %assign i i+1 
  %endrep

在GAS中,与.set等价的东西是可能的。
%xdefinewould be usable, too,尽管这会使汇编器每次都对增长的0+1+1+1+1+...文本字符串求值。
相反,您的想法在NASM语法中看起来是这样的,其中注解和标签名称提醒读者它是如何工作的。实际上我更喜欢这个版本而不是%assign版本;需要跟踪的事情就少了。

vec64_0to63:
%rep 64
    db $-v2       ; 0..63  value = offset
%endrep

使用times在一行中执行所有操作是行不通的:v2: times 16 db $-v2会填入零,因为$-v2在重复之前会评估为常数零。

相关问题