【问题标题】:Filling an AVX512 register with incrementing bytes用递增字节填充 AVX512 寄存器
【发布时间】:2022-11-28 05:53:20
【问题描述】:

是否有任何不明显的技巧可以用递增字节(小端)填充 AVX512 寄存器?也就是说,等效于此代码:

__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);
}

我看到的唯一明显的方法(以及 GCC 使用上述代码生成的方法)只是采用从内存中使用 vmovdqa64 的通用方法——但这个常数的熵足够低,看起来应该是能够以某种方式做得更好。

(我知道通常常量加载通常不在关键路径中,或者您有一个备用寄存器专用于常量以便能够重新加载它,但我很想知道该指令集中是否隐藏了任何技巧。例如,对于具有全角寄存器乘法的指令集,您可以用 0x1 填充每个字节,对寄存器进行平方,然后将结果左移一位——但据我所知,这不适合 AVX512 .)

【问题讨论】:

  • 我有点掩饰哪个AVX512 扩展在这里,相当刻意。如果答案是“嘿,这个晦涩的扩展中有这个还不可用的巧妙东西”,我仍然学到了一些东西:-)
  • 不幸的是,我不知道 asm 中有任何模式/序列技巧。很容易得到相同的每个元素中的东西,如 What are the best instruction sequences to generate vector constants on the fly? ,但没有什么自然地对每个元素做不同的事情。充其量我可以想象它可能会做一些洗牌/添加步骤来建立一个具有 log2 64 步的扩展模式,但我不确定这些步骤可能是什么样子,而且这是很多操作与操作。负载。
  • @PeterCordes - 谢谢。正如您所指出的,我刚刚进入 AVX512,还没有弄清楚所有事情。我还应该在代码中做一个明确的注释,我希望整个循环不断传播出去,只留下负载。我现在就这样做。
  • 顺便说一句,这实际上是一个内在问题吗?我注意到你问题中的代码是用 C 或 C++ 编写的。即使你想出了一个聪明的方法来在几条指令中生成一个常量,一些编译器(gcc 和 clang)也会通过它进行 constprop 并生成一个 64 字节的常量,无论你是否愿意。自己选择如何在寄存器中生成常量通常只能在 asm 中选择。
  • 哦,你是对的,我在看 0x0101010 ** 2 的高半部分得到 0x1020304030201。对于小端机器,低半部分的顺序是正确的,高半部分是相反的。 (而且它甚至不是高半位,只是高 56 位。)

标签: assembly optimization x86-64 micro-optimization avx512


【解决方案1】:

我不认为有任何非常有效的方法来处理不同元素具有不同值的动态序列。如果您不能利用与先前元素的相似性,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, 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

我确实确认这适用于附加到 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}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-01
    • 1970-01-01
    • 2016-09-24
    • 2021-10-27
    • 2014-11-18
    • 1970-01-01
    相关资源
    最近更新 更多