Assembly 如何在AVX寄存器上打包16位寄存器/变量
我使用内联汇编,我的代码如下:Assembly 如何在AVX寄存器上打包16位寄存器/变量,assembly,x86,sse,avx,inline-assembly,Assembly,X86,Sse,Avx,Inline Assembly,我使用内联汇编,我的代码如下: __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, %
__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]];
}
内联汇编程序与C代码稍有相似之处,因此我倾向于假设这两个代码是相同的 这主要是一种观点,但我建议使用扩展汇编程序,而不是使用扩展汇编程序。内部函数允许编译器进行寄存器分配和变量优化,以及可移植性——在没有目标指令集的情况下,每个向量操作都可以由函数模拟 下一个问题是内联源代码似乎只处理两个索引的替换块
arr[i]=s16[arr[i]
。使用AVX2,这应该通过两个聚集操作来完成,因为Y寄存器只能容纳8个uint32或到查找表的偏移量,或者当它可用时,替换阶段应该由可以并行运行的分析函数来执行
使用intrinsic,该操作可能看起来像这样
__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);
}
通过优化,一个好的编译器将为每个语句生成大约一条汇编指令;如果不进行优化,所有中间变量都会溢出到堆栈中,以便轻松调试。内联汇编程序与C代码稍有相似,因此我倾向于假设这两个变量是相同的 这主要是一种观点,但我建议使用扩展汇编程序,而不是使用扩展汇编程序。内部函数允许编译器进行寄存器分配和变量优化,以及可移植性——在没有目标指令集的情况下,每个向量操作都可以由函数模拟 下一个问题是内联源代码似乎只处理两个索引的替换块
arr[i]=s16[arr[i]
。使用AVX2,这应该通过两个聚集操作来完成,因为Y寄存器只能容纳8个uint32或到查找表的偏移量,或者当它可用时,替换阶段应该由可以并行运行的分析函数来执行
使用intrinsic,该操作可能看起来像这样
__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);
}
通过优化,一个好的编译器将为每个语句生成大约一条汇编指令;如果没有优化,所有中间变量都会溢出到堆栈中,以便轻松调试。一个Skylake(聚集速度很快),使用Aki的答案将两个聚集链接在一起可能是一个胜利。这可以让你使用向量整数的东西非常有效地进行旋转
在Haswell上,继续使用标量代码可能会更快,这取决于周围代码的外观。(或者用矢量代码执行矢量旋转+xor操作仍然是一个成功。试试看。)
您有一个非常糟糕的性能错误,还有几个其他问题:
"pxor %0, %0 \n\t"
"vpinsrw $0, %%ecx, %0, %0 \n\t"
使用传统SSEpxor
将%0
的低位128b归零,同时保持高位128b不变,将导致Haswell受到SSE-AVX过渡处罚;我想在pxor
和第一个vpinsrw
上各有大约70个循环,并且有一个虚假的依赖关系
相反,使用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"
...
当您可以直接插入向量时,将指令(和UOP)加载到整数寄存器,然后再从整数寄存器进入向量,这是一种巨大的浪费
您的索引已经是零扩展的,所以我使用64位寻址模式来避免在每条指令上浪费地址大小前缀。(由于您的表是静态的,它位于低2G的虚拟地址空间中(在默认的代码模型中),因此32位寻址确实有效,但它没有给您带来任何好处。)
不久前,我尝试将标量LUT结果(对于GF16乘法)转换为向量,并针对Intel Sandybridge进行调优。不过,我没有像你那样把LUT查找链接起来。看见当我发现GF16作为4位LUT使用时效率更高,但不管怎样,我发现如果没有收集指令,从内存到向量的pinsrw
是好的
您可能希望通过同时在两个向量上交错操作来提供更多ILP。或者甚至可能进入4个向量的低64b,并与vpunpcklqdq
结合。(vmovd
比vpinsrw
更快,因此在uop吞吐量上几乎是收支平衡。)
这些可以而且应该是
xor%[subkey],%%ecx
。32位操作数大小在这里更有效,只要输入没有在上16位中设置任何位,就可以正常工作。使用[subkey]“ri”(subkey)
约束允许在编译时已知立即值。(这可能更好,并且稍微降低了寄存器压力,但由于您多次使用它,因此以牺牲代码大小为代价。)
但是rolw
指令必须保持16位
您可以考虑将两个或四个值打包成整数登记器(用<代码> MOVZWL S16(…))、%%ECX < />代码> SHL $ 16、%%ECX < /C> > />代码> MOVS16(…)、%CX/<代码> SL$ 16、%%RCX < /代码>……,但是您必须用移位/ /或掩蔽来模拟旋转。然后再次解包以将其作为索引重用
在两个LUT查找之间出现整数太糟糕了,否则在解包之前可以在向量中进行
您可以选择提取16b向量块的策略,这看起来非常好
movdq
从xmm到GP寄存器在Haswell/Skylake上的端口0上运行,并且shr
/ror
"xorw %4, %%cx \n\t"
"xorw %4, %%dx \n\t"
// 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);
}
// 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;
}
if (x > 65535)
__builtin_unreachable();