我使用内联汇编,我的代码如下:
__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"
答案 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
结合。 (vpunpcklqdq
比vmovd
更快,所以它在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的一些提示,请参阅x86 tag wiki的结尾。
查看编译器生成的代码,该代码在您的asm和编译器正在执行的操作之间进行接口:可能您的约束错误。
也许你混淆了vmovq
或%0
或其他什么。我绝对建议使用%1
而不是操作数。另请参阅inline-assembly tag wiki以获取指南的链接。
根本不需要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);
}
通过优化,一个不错的编译器将为每个语句生成大约一个汇编指令;如果没有优化,所有中间变量都会溢出到堆栈中以便于调试。