【问题标题】:byte array permute SSE optimization字节数组置换 SSE 优化
【发布时间】:2017-05-20 23:16:10
【问题描述】:

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

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);
  }
}

我花了一个小时思考如何将代码翻译成pshufb。是否可以使用单个 pshufb 置换 16 字节,还是需要多条指令?足够好的解决方案一次只置换 16 个字节。

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

【问题讨论】:

  • k 的可能值范围是多少? s 的典型尺寸是多少?
  • 此链接应该提供您想知道的所有内容:msdn.microsoft.com/en-us/library/bb531427(v=vs.120).aspx
  • 这是一个相当奇怪的排列。对于所有j 这样j! > k,这将只是将s[j]s[0] 交换,这将插入只是将原始s[j] 向上移动一个,并将最后一个留在s[0] 中。
  • 即使您使用的是 64 位整数(不太可能),这对于所有 j > 20 都是正确的。对于 32 位整数,对于所有 j > 12 都是正确的. 如果您要置换大于该值的向量,您可能希望在 k == 0 时中断,并对剩余的幻灯片使用 memmove。如果您置换的向量少于此值,我很难相信这是您的性能瓶颈。
  • s 的典型大小是 16 字节的倍数。仅针对 16 个字节的解决方案是可以的。 k 索引在相当紧密的循环中递增,直到置换结果符合某些条件或 k 会溢出。我还根据条件限制k 的大小。

标签: c++ gcc x86-64 sse simd


【解决方案1】:

单次调用

请注意,您可以使用mixed radix 在位置符号系统中记下数字k,以便此表示中的每个数字都可以为j 的几个连续值定义交换元素的索引。

例如,对于长度为 12 的字符串,您可以将任何 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)

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

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

//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;
}

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

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

很多电话

不幸的是,索引计算在大多数情况下仍然会使用建议的解决方案,请参阅generated assembly。如果您一次处理多个连续的 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
        }
    }
}

这样,您平均需要对每个排列进行一次随机播放(请参阅assembly),这似乎接近完美。

代码和结果

这是所有解决方案中的full working code

请注意,查找表的生成并非易事,无法完全解释,并且相应的代码相当大(并且充满了令人讨厌的细节)。

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)

所以单调用版本比原始代码快 20 倍,多调用版本比单调用版本快 6.5 倍.

【讨论】:

  • 不错的分析!
  • 确实很好。 :) 我对如何填充查找数组感到有些困惑,但我会研究一下。
  • 不错的答案!一件事,也许不是一遍又一遍地改组s,可以将三个改组的结果保存到三个不同的变量中,然后加入结果(OR 可以,改组可以零元素)。这可能会打破长依赖链并更快地执行。
  • @MargaretBloom:不是真的,因为每次洗牌都取决于前一次洗牌的结果。我想通过k 部分展开外循环会更好(鉴于k 的迭代不相互依赖)。
  • 我的意思是让洗牌相互依赖。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-09
  • 2021-06-10
  • 2019-09-20
  • 1970-01-01
  • 1970-01-01
  • 2011-12-16
相关资源
最近更新 更多