[英]How to pack 16 16-bit registers/variables on AVX registers
我使用内联汇编,我的代码如下:
__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"
一个Skylake(聚集速度很快),使用Aki的答案将两个聚集链接在一起可能是一次胜利。 这样一来,您就可以使用向量整数填充非常有效地进行旋转。
在Haswell上,继续使用标量代码可能会更快,具体取决于周围代码的外观。 (或者用矢量代码进行矢量rotate + xor仍然是一个胜利。尝试一下。)
您有一个非常糟糕的性能错误,还有两个其他问题:
"pxor %0, %0 \n\t"
"vpinsrw $0, %%ecx, %0, %0 \n\t"
使用传统SSE pxor
将%0
的低位128b设为零,而未修改高位128b将导致Haswell发生SSE-AVX过渡损失; 我认为,在pxor
和第一个vpinsrw
上每个周期大约70个周期。 在Skylake上,它只会稍慢一些 ,并且具有错误的依赖关系。
而是使用vmovd %%ecx, %0
将向量reg的高字节清零(从而打破对旧值的依赖性)。
其实使用
"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"
...
当您可以直接插入vectors时,将它们加载到整型寄存器中然后再转入vectors会浪费大量的指令(和uops) 。
您的索引已被零扩展,因此我使用64位寻址模式以避免在每条指令上浪费地址大小的前缀。 (由于您的表是static
,因此它处于2G的虚拟地址空间的低位(在默认代码模型中),因此32位寻址确实有效,但是却无济于事。)
我前段时间做了实验,将标量LUT结果(对于GF16乘以)转换为向量,并针对Intel Sandybridge进行了调整。 不过,我并没有像您那样链接LUT查找。 参见https://github.com/pcordes/par2-asm-experiments 。 在发现GF16使用pshufb
作为4位LUT更有效之后,我有点放弃了它,但是无论如何,我发现如果没有收集指令,从内存到向量的pinsrw
很好。
您可能希望通过一次对两个向量进行交织操作来提供更多的ILP。 或者甚至进入4个向量的低64b,然后与vpunpcklqdq
结合。 ( vmovd
更快,因此vpinsrw
吞吐量几乎可以达到收支平衡。)
"xorw %4, %%cx \n\t"
"xorw %4, %%dx \n\t"
这些可以并且应该是xor %[subkey], %%ecx
。 32位操作数大小在这里更有效,并且只要您的输入的高16位没有设置任何位,它就可以很好地工作。使用[subkey] "ri" (subkey)
约束可以在输入时使用立即数在编译时已知。 (这可能更好,并且可以稍微降低寄存器压力,但是由于您多次使用它,因此以代码大小为代价。)
不过, rolw
指令必须保留16位。
您可以考虑将两个或四个值打包到整数寄存器中(使用movzwl s16(...), %%ecx
/ shl $16, %%ecx
/ mov s16(...), %cx
/ shl $16, %%rcx
/ ...),但随后您必须使用移位/或和遮罩来模拟旋转。 并再次解压缩以将其重新用作索引。
整数填充在两次LUT查找之间是非常糟糕的,否则您可以在解压缩之前在向量中进行处理。
您提取向量的16b块的策略看起来不错。 从xmm到GP寄存器的movdq
在Haswell / Skylake的端口0上运行,而shr
/ ror
在端口0 /端口6上运行。 因此,您确实需要争夺端口,但是存储整个向量并重新加载它会占用更多的加载端口。
可能值得尝试进行256b的存储,但是仍然可以从vmovq
获得低64b的存储,因此可以在没有太多延迟的情况下启动前4个元素。
至于得到错误的答案:请使用调试器。 调试器对于asm的工作非常好; 有关使用GDB的一些提示,请参见x86 标签Wiki的末尾。
查看在您的asm与编译器正在执行的操作之间生成的编译器生成的代码:也许您遇到了约束错误。
也许您与%0
或%1
东西混在一起了。 我绝对建议使用%[name]
代替操作数。 另请参阅inline-assembly 标签wiki ,以获取指南的链接。
您根本不需要inline-asm,除非编译器在将向量解压缩为16位元素并且不生成所需代码的过程中做得很糟糕。 https://gcc.gnu.org/wiki/DontUseInlineAsm
我将其放在Matt Godbolt的编译器资源管理器中 ,您可以在其中看到asm输出。
// 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
直接从LUT转换为第一个s16[idx]
的向量。 否则,gcc会先将movzx负载加载到整数reg中,然后再从其中加载vmovd
。 这样可以避免缓存行拆分或页面拆分进行32位加载的任何风险,但是对于平均吞吐量而言,这种风险值得承担,因为这可能会限制前端uop吞吐量。
注意__rolw
中__rolw的使用。 gcc支持它,但是clang不支持 。 无需额外的指令即可编译为16位循环。
不幸的是,gcc并没有意识到16位的旋转会将寄存器的高位保持为零,因此在使用%rdx
作为索引之前,它会进行毫无意义的movzwl %dx, %edx
。 即使使用gcc7.1和8-snapshot,这也是一个问题。
顺便说一句,gcc将s16
表地址加载到寄存器中,因此它可以使用诸如vmovd (%rcx,%rdx,2), %xmm0
类的寻址模式vmovd (%rcx,%rdx,2), %xmm0
而不是将4字节地址嵌入到每个指令中。
由于多余的movzx
是gcc唯一比您手可以做的事情差的事情,因此您可能会考虑在gcc认为需要32或64位输入寄存器的内联asm中制作一个7旋转功能。 (使用类似的方法来获得“一半”大小的旋转,即16位:
// 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;
}
但是,即使这样,gcc仍然希望发出无用的movzx
或movl
指令。 通过为idx
使用更广泛的类型,我摆脱了一些零扩展的问题,但是仍然存在问题。 ( 源于编译器资源管理器 )。 出于某种原因,让subkey
使用函数arg而不是编译时常量会有所帮助。
您也许可以让gcc假设某物是零扩展的16位值,其中包括:
if (x > 65535)
__builtin_unreachable();
然后,您可以完全删除任何嵌入式asm,只需使用__rolw
。
但是请注意, icc
会将其编译为实际检查,然后跳转到函数末尾。 它应该适用于gcc,但我没有测试。
但是,如果花了很多时间才能使编译器不致于陷入僵局,则只用内联asm编写整个代码是很合理的。
内联汇编程序与C代码略有相似,因此我很想假设这两个代码是相同的。
这主要是一种意见,但我建议使用内部函数而不是扩展汇编程序。 内部特性允许编译器完成寄存器分配和变量优化以及可移植性-每个向量操作都可以在没有目标指令集的情况下由函数进行仿真。
下一个问题是内联源代码似乎只处理两个索引i
的替换块arr[i] = s16[arr[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);
}
通过优化,一个不错的编译器将为每个语句生成大约一个汇编程序指令。 没有优化,所有中间变量都会溢出到堆栈中,以便于调试。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.