如何在AVX寄存器

时间:2017-08-11 03:33:46

标签: assembly x86 sse avx

我使用内联汇编,我的代码如下:

__m128i inl = _mm256_castsi256_si128(in);
__m128i inh = _mm256_extractf128_si256(in, 1); 
__m128i outl, outh;
__asm__(
    "vmovq %2, %%rax                        \n\t"
    "movzwl %%ax, %%ecx                     \n\t"
    "shr $16, %%rax                         \n\t"
    "movzwl %%ax, %%edx                     \n\t"
    "movzwl s16(%%ecx, %%ecx), %%ecx        \n\t"
    "movzwl s16(%%edx, %%edx), %%edx        \n\t"
    "xorw %4, %%cx                          \n\t"
    "xorw %4, %%dx                          \n\t"
    "rolw $7, %%cx                          \n\t"
    "rolw $7, %%dx                          \n\t"
    "movzwl s16(%%ecx, %%ecx), %%ecx        \n\t"
    "movzwl s16(%%edx, %%edx), %%edx        \n\t"
    "pxor %0, %0                            \n\t"
    "vpinsrw $0, %%ecx, %0, %0              \n\t"
    "vpinsrw $1, %%edx, %0, %0              \n\t"

: "=x" (outl), "=x" (outh)
: "x" (inl), "x" (inh), "r" (subkey)
: "%rax", "%rcx", "%rdx"
);

我在代码中省略了一些vpinsrw,这或多或少地显示了原理。真正的代码使用16个vpinsrw操作。但输出并不符合预期。

b0f0 849f 446b 4e4e e553 b53b 44f7 552b 67d  1476 a3c7 ede8 3a1f f26c 6327 bbde
e553 b53b 44f7 552b    0    0    0    0 b4b3 d03e 6d4b c5ba 6680 1440 c688 ea36

第一行是真正的答案,第二行是我的结果。 C代码在这里:

for(i = 0; i < 16; i++)
{  
    arr[i] = (u16)(s16[arr[i]] ^ subkey);
    arr[i] = (arr[i] << 7) | (arr[i] >> 9);
    arr[i] = s16[arr[i]];

}

我的任务是让这段代码更快。

在旧代码中,数据从ymm移到堆栈,然后像这样从堆栈移到16字节寄存器。所以我想将数据直接从ymm移动到16字节寄存器。

__asm__(     

    "vmovdqa %0, -0xb0(%%rbp)               \n\t"

    "movzwl -0xb0(%%rbp), %%ecx             \n\t"
    "movzwl -0xae(%%rbp), %%eax             \n\t"
    "movzwl s16(%%ecx, %%ecx), %%ecx        \n\t"
    "movzwl s16(%%eax, %%eax), %%eax        \n\t"
    "xorw %1, %%cx                          \n\t"
    "xorw %1, %%ax                          \n\t"
    "rolw $7, %%cx                          \n\t"
    "rolw $7, %%ax                          \n\t"
    "movzwl s16(%%ecx, %%ecx), %%ecx        \n\t"
    "movzwl s16(%%eax, %%eax), %%eax        \n\t"
    "movw %%cx, -0xb0(%%rbp)                \n\t"
    "movw %%ax, -0xae(%%rbp)                \n\t"

2 个答案:

答案 0 :(得分:4)

Skylake(聚会很快),使用Aki的答案将两个人聚集在一起可能是一场胜利。这可以让你使用向量整数的东西非常有效地进行旋转。

在Haswell上,继续使用标量代码可能会更快,具体取决于周围代码的外观。 (或者也许用向量代码做向量旋转+ xor仍然是一个胜利。试试看看。)

你有一个非常糟糕的性能错误,以及其他一些问题:

"pxor %0, %0                            \n\t"
"vpinsrw $0, %%ecx, %0, %0              \n\t"

使用legacy-SSE pxor将{128}的低{128}归零,同时保持上部128b不被修改将导致Haswell的SSE-AVX转换惩罚;我认为%0和第一pxor各约70个周期。 On Skylake, it will only be slightly slower,并且具有错误的依赖性。

相反,使用vpinsrw ,它将向量reg的高位字节归零(从而打破了对旧值的依赖)。

实际上,请使用

vmovd %%ecx, %0

当你可以直接插入向量时,加载到整数寄存器然后从那里进入向量就会浪费大量的指令(和uops)。

您的索引已经零扩展,因此我使用64位寻址模式以避免在每条指令上浪费地址大小前缀。 (由于您的表是"vmovd s16(%%rcx, %%rcx), %0 \n\t" // leaves garbage in element 1, which you over-write right away "vpinsrw $1, s16(%%rdx, %%rdx), %0, %0 \n\t" ... ,它位于虚拟地址空间的低2G(在默认代码模型中),因此32位寻址确实起作用,但它没有获得任何东西。)< / p>

我前段时间实验过,将标量LUT结果(GF16乘法)转换为向量,调整为英特尔Sandybridge。不过,我并没有像你那样链接LUT查找。见https://github.com/pcordes/par2-asm-experiments。我发现GF16在static作为4位LUT时效率更高,但我发现pshufb从记忆中变成向量是好的,如果你不这样做的话,我就放弃了它。收集指示。

您可能希望通过一次对两个向量进行交错操作来提供更多ILP。或者甚至可能进入4个向量的低64b,并与pinsrw结合。 (vpunpcklqdqvmovd更快,所以它在uop吞吐量方面非常均衡。)

vpinsrw

这些可以而且应该是"xorw %4, %%cx \n\t" "xorw %4, %%dx \n\t" 。 32位操作数大小在这里效率更高,并且只要您的输入没有在高位16中设置任何位,就可以正常工作。使用xor %[subkey], %%ecx约束时允许立即值为#39 ;在编译时已知。 (这可能会更好,并且会略微降低寄存器压力,但是由于您多次使用它会牺牲代码大小。)

[subkey] "ri" (subkey)指令必须保持16位。

你可以考虑将两个或四个值打包成一个整数寄存器(rolw / movzwl s16(...), %%ecx / shl $16, %%ecx / mov s16(...), %cx / ...),但是你&# 39; d必须通过移位/或屏蔽来模拟旋转。然后再次解压缩以将它们重新用作索引。

两个LUT查找之间的整数内容太糟糕了,否则你可以在解包之前在向量中完成。

你提取16b块矢量的策略看起来很不错。从xmm到GP寄存器的shl $16, %%rcx在Haswell / Skylake上的端口0上运行,movdq / shr在port0 / port6上运行。所以你确实竞争端口,但存储整个矢量并重新加载它将需要更多的加载端口。

可能值得尝试做256b商店,但仍然从ror获得低64b,所以前4个元素可以在没有太多延迟的情况下开始。

至于得到错误的答案:使用调试器。调试器对asm工作得很好;有关使用GDB的一些提示,请参阅 tag wiki的结尾。

查看编译器生成的代码,该代码在您的asm和编译器正在执行的操作之间进行接口:可能您的约束错误。

也许你混淆了vmovq%0或其他什么。我绝对建议使用%1而不是操作数。另请参阅 tag wiki以获取指南的链接。

C版本避免内联asm(但gcc浪费了指令)。

根本不需要inline-asm,除非你的编译器将向量解包为16位元素并且没有生成你想要的代码。 https://gcc.gnu.org/wiki/DontUseInlineAsm

我把这个up on Matt Godbolt's compiler explorer放在你可以看到asm输出的地方。

%[name]

我必须使用指针转换来直接从LUT获取// This probably compiles to code like your inline asm #include <x86intrin.h> #include <stdint.h> extern const uint16_t s16[]; __m256i LUT_elements(__m256i in) { __m128i inl = _mm256_castsi256_si128(in); __m128i inh = _mm256_extractf128_si256(in, 1); unsigned subkey = 8; uint64_t low4 = _mm_cvtsi128_si64(inl); // movq extract the first elements unsigned idx = (uint16_t)low4; low4 >>= 16; idx = s16[idx] ^ subkey; idx = __rolw(idx, 7); // cast to a 32-bit pointer to convince gcc to movd directly from memory // the strict-aliasing violation won't hurt since the table is const. __m128i outl = _mm_cvtsi32_si128(*(const uint32_t*)&s16[idx]); unsigned idx2 = (uint16_t)low4; idx2 = s16[idx2] ^ subkey; idx2 = __rolw(idx2, 7); outl = _mm_insert_epi16(outl, s16[idx2], 1); // ... do the rest of the elements __m128i outh = _mm_setzero_si128(); // dummy upper half return _mm256_inserti128_si256(_mm256_castsi128_si256(outl), outh, 1); } 到第一个vmovd的向量。没有它,gcc将movzx加载到整数reg中,然后从那里使用s16[idx]。这样可以避免高速缓存行拆分或页面拆分带来32位负载的风险,但这种风险对于平均吞吐量来说可能是值得的,因为这可能是前端uop吞吐量的瓶颈。

请注意x86intrin.h中vmovd的使用。 gcc supports it, but clang doesn't。它编译为16位旋转,没有额外的指令。

不幸的是,gcc没有意识到16位旋转会使寄存器的高位保持为零,因此在使用__rolw作为索引之前,它会做一个无意义的movzwl %dx, %edx。即使使用gcc7.1和8-snapshot,这也是一个问题。

BTW,gcc将%rdx表地址加载到寄存器中,因此它可以使用s16等寻址模式,而不是将4字节地址嵌入到每条指令中。

由于额外的vmovd (%rcx,%rdx,2), %xmm0是gcc唯一比你手工做的更糟糕的事情,你可能会考虑在内联asm中制作一个7位旋转函数,gcc认为它需要32或64位输入寄存器。 (使用类似的东西得到一个&#34;一半&#34;大小的旋转,即16位:

movzx

然而,即便如此,gcc仍然希望发出无用的// pointer-width integers don't need to be re-extended // but since gcc doesn't understand the asm, it thinks the whole 64-bit result may be non-zero static inline uintptr_t my_rolw(uintptr_t a, int count) { asm("rolw %b[count], %w[val]" : [val]"+r"(a) : [count]"ic"(count)); return a; } movzx指令。我通过使用movl的更宽类型来摆脱一些零扩展,但仍然存在问题。 (source on the compiler explorer)。由于某种原因,使用idx函数arg而不是编译时常量会有所帮助。

你可能能够让gcc假设某些东西是零扩展的16位值:

subkey

然后你可以完全删除任何内联asm,只需使用if (x > 65535) __builtin_unreachable();

但要注意__rolw会将其编译为实际检查,然后超出函数末尾跳转。它应该适用于gcc,但我没有测试过。

如果需要进行这么多的调整以使编译器不要在脚下射击,那么将整个内容写入内联asm是非常合理的。

答案 1 :(得分:2)

内联汇编程序略微类似于C代码,因此我很想假设这两个代码是相同的。

这主要是一种观点,但我建议使用intrinsics而不是扩展的汇编程序。内在函数允许编译器完成寄存器分配和变量优化,以及可移植性 - 在没有目标指令集的情况下,每个向量操作都可以由函数模拟。

下一个问题是,内联源代码似乎只处理两个索引arr[i] = s16[arr[i]]的替换块i。使用AVX2,这应该通过两个收集操作完成,因为Y寄存器只能保存8个uint32_ts或偏移到查找表,或者当它可用时,替换阶段应该通过分析来执行可以并行运行的函数。

使用内在函数,操作可能看起来像这样。

__m256i function(uint16_t *input_array, uint16_t subkey) {
  __m256i array = _mm256_loadu_si256((__m256i*)input_array);
          array = _mm256_xor_si256(array, _mm256_set_epi16(subkey));
  __m256i even_sequence = _mm256_and_si256(array, _mm256_set_epi32(0xffff));
  __m256i odd_sequence = _mm256_srli_epi32(array, 16);
  even_sequence = _mm256_gather_epi32(LUT, even_sequence, 4);
  odd_sequence = _mm256_gather_epi32(LUT, odd_sequence, 4);
  // rotate
  __m256i hi = _mm256_slli_epi16(even_sequence, 7);
  __m256i lo = _mm256_srli_epi16(even_sequence, 9);
  even_sequence = _mm256_or_si256(hi, lo);
  // same for odd
  hi = _mm256_slli_epi16(odd_sequence, 7);
  lo = _mm256_srli_epi16(odd_sequence, 9);
  odd_sequence = _mm256_or_si256(hi, lo);
  // Another substitution
  even_sequence = _mm256_gather_epi32(LUT, even_sequence, 4);
  odd_sequence = _mm256_gather_epi32(LUT, odd_sequence, 4);
  // recombine -- shift odd by 16 and OR
  odd_sequence = _mm256_slli_epi32(odd_sequence, 16);
  return _mm256_or_si256(even_sequence, odd_sequence);

}

通过优化,一个不错的编译器将为每个语句生成大约一个汇编指令;如果没有优化,所有中间变量都会溢出到堆栈中以便于调试。