字节数组置换SSE优化

JAT*_*rim 12 c++ gcc sse x86-64 simd

我想使用SSE内在函数翻译此代码.我找到了可能与此代码一起使用的pshufbSSSE3指令和类似的__builtin_ia32_pshufb128(v128i, v128i)GCC内在函数.代码s通过索引k通过特定方式交换数组中的字节来置换字节向量.

void permutation(int k, std::vector<char> & s) 
{
  for(size_t j = 1; j < s.size(); ++j) 
  {
      std::swap(s[k % (j + 1)], s[j]); 
      k = k / (j + 1);
  }
}
Run Code Online (Sandbox Code Playgroud)

我花了一个小时思考如何将代码翻译成pshufb.是否有可能用单个信号置换16个字节,pshufb还是需要多个指令?足够好的解决方案将在时间上只置换16个字节.

编辑:问题的进一步背景:我正在迭代所有可能的排列s.提前计算k = 0, 1, 2,...多个结果s是可以的.但是我需要k稍后再现-th置换,最好是O(1)操作.

stg*_*lov 3

单次通话

请注意,您可以使用混合基数 记k下位置表示法系统中的数字,以便此表示中的每个数字将为 的多个连续值定义交换元素的索引。j

例如,对于长度为 12 的字符串,您可以将 any 写k为带基数的三位数:

720 = 1*2*3*4*5*6  (0-th digit, lowest value)
504 = 7*8*9        (1-th digit)
1320 = 10*11*12    (2-th digit, highest value)
Run Code Online (Sandbox Code Playgroud)

现在,您可以为每个位置以及该位置中的每个数字值预先计算所有元素的累积排列,并将其保存在查找表中。然后你就可以通过一条指令进行多次交换。

这是一个示例(预计算是最难的部分):

//to be precomputed:
__m128i mask0[ 720];
__m128i mask1[ 504];
__m128i mask2[1320];

__m128i permutation(int k, __m128i s) {
    s = _mm_shuffle_epi8(s, mask0[k %  720]); k /=  720;  //j = [1..5]
    s = _mm_shuffle_epi8(s, mask1[k %  504]); k /=  504;  //j = [6..8]
    s = _mm_shuffle_epi8(s, mask2[k       ]);             //j = [9..11]
    return s;
}
Run Code Online (Sandbox Code Playgroud)

您可以改变基数的分解,以便在步骤数和查找表的大小之间取得平衡。

注意:除法运算非常慢。仅使用编译时常量的除法,然后优化器会将它们转换为乘法。检查生成的程序集以确保其中不存在除法指令。

很多电话

不幸的是,索引计算仍然会占用建议解决方案的大部分时间,请参阅生成的程序集k如果您一次处理多个连续值,则可以显着减少此开销。

优化解决方案的最简单方法是:分别迭代 的数字,k而不是对 进行单个循环k。那么索引计算就变得不必要了。此外,您还可以重复使用部分计算结果。

__m128i s;// = ???
for (int k0 = 0; k0 <  720; k0++) {
    __m128i s0 = _mm_shuffle_epi8(s, mask0[k0]);
    for (int k1 = 0; k1 <  504; k1++) {
        __m128i s1 = _mm_shuffle_epi8(s0, mask1[k1]);
        for (int k2 = 0; k2 < 1320; k2+=4) {
            //for k = (((k2+0) * BASE1) + k1) * BASE0 + k0:
            __m128i sx0 = _mm_shuffle_epi8(s1, mask2[k2+0]);
            //for k = (((k2+1) * BASE1) + k1) * BASE0 + k0:
            __m128i sx1 = _mm_shuffle_epi8(s1, mask2[k2+1]);
            //for k = (((k2+2) * BASE1) + k1) * BASE0 + k0:
            __m128i sx2 = _mm_shuffle_epi8(s1, mask2[k2+2]);
            //for k = (((k2+3) * BASE1) + k1) * BASE0 + k0:
            __m128i sx3 = _mm_shuffle_epi8(s1, mask2[k2+3]);

            // ... check four strings: sx0, sx1, sx2, sx3
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,您平均需要对每个排列进行一次洗牌(请参阅汇编),这看起来接近完美。

代码和结果

这是所有解决方案的完整工作代码。

请注意,要完全解释查找表的生成并不简单,并且相应的代码相当大(并且充满了令人讨厌的细节)。

在Intel Core 2 Duo E4700 Allendale (2600MHz)上运行的基准测试得出结果:

2.605 s: original code         (k < 12739451)
0.125 s: single-call fast code (k < 12739451)
4.822 s: single-call fast code (k < 479001600)
0.749 s: many-call fast code   (k < 479001600)
Run Code Online (Sandbox Code Playgroud)

因此,单调用版本比原始代码快约20倍,而多次调用版本比单调用版本快约6.5倍。