在SSE __m128i寄存器中交换字节

9
我是一位有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

我有以下问题:

__m128i寄存器中,有16个8位值,按以下顺序排列:

[ 1, 5, 9, 13 ] [ 2, 6, 10, 14] [3, 7, 11, 15]  [4, 8, 12, 16]

我希望能够高效地对字节进行洗牌,以获得这种排序方式:
[ 1, 2, 3, 4 ] [ 5, 6, 7, 8] [9, 10, 11, 12]  [13, 14, 15, 16]

实际上,这类似于对8位元素的一个寄存器进行4x4矩阵转置。

请问您能告诉我哪些SSE(最好是<= SSE2)指令适合实现这一功能吗?


2
使用SSSE3,有“pshufb”,否则代码会变得有点混乱。 - harold
谢谢你指引我使用pshufb!我想我可以从那里开始 :) - born49
除了SSSE3的PSHUFB,这在旧CPU上有点像是Intel的专属功能之外,你需要看一下一系列混乱的SSE2 PUNPCK(L/H)BW指令,正如@harold所指出的那样。 - Iwillnotexist Idonotexist
1个回答

5
你真的需要使用SSSE3,这比尝试使用<= SSE2更加干净。
你的代码将类似于这样:
   #include <tmmintrin.h> // _mm_shuffle_epi8
   #include <tmmintrin.h> // _mm_set_epi8
   ...
   // check if your hardware supports SSSE3
   ...
   __m128i mask = _mm_set_epi8(15, 11, 7, 3,
                               14, 10, 6, 2,
                               13,  9, 5, 1,
                               12,  8, 4, 0);
   __m128i mtrx = _mm_set_epi8(16, 12, 8, 4,
                               15, 11, 7, 3,
                               14, 10, 6, 2,
                               13,  9, 5, 1);
   mtrx         = _mm_shuffle_epi8(mtrx, mask);

如果您确实需要SSE2,那么以下内容就足够了:
(假设我正确解释了您最初的顺序)
  __m128i mask = _mm_set_epi8(0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF);
  __m128i mtrx = _mm_set_epi8(16, 12, 8, 4,
                              15, 11, 7, 3,
                              14, 10, 6, 2,
                              13,  9, 5, 1);                                   // [1, 5, 9, 13] [2,  6, 10, 14] [3,  7, 11, 15] [ 4,  8, 12, 16]
  mtrx = _mm_packus_epi16(_mm_and_si128(mtrx, mask), _mm_srli_epi16(mtrx, 8)); // [1, 9, 2, 10] [3, 11,  4, 12] [5, 13,  6, 14] [ 7, 15,  8, 16]
  mtrx = _mm_packus_epi16(_mm_and_si128(mtrx, mask), _mm_srli_epi16(mtrx, 8)); // [1, 2, 3,  4] [5,  6,  7,  8] [9, 10, 11, 12] [13, 14, 15, 16]

更容易调试:
  __m128i mtrx = _mm_set_epi8(16, 12, 8, 4,
                              15, 11, 7, 3,
                              14, 10, 6, 2,
                              13, 9, 5, 1);            // [1, 5,  9, 13] [ 2,  6, 10, 14] [ 3,  7, 11, 15] [ 4,  8, 12, 16]
  __m128i mask = _mm_set_epi8(0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF,
                              0x00, 0xFF, 0x00, 0xFF);
  __m128i temp = _mm_srli_epi16(mtrx, 8);              // [5, 0, 13,  0] [ 6,  0, 14,  0] [ 7,  0, 15,  0] [ 8,  0, 16,  0]
  mtrx         = _mm_and_si128(mtrx, mask);            // [1, 0,  9,  0] [ 2,  0, 10,  0] [ 3,  0, 11,  0] [ 4,  0, 12,  0]
  mtrx         = _mm_packus_epi16(mtrx, temp);         // [1, 9,  2, 10] [ 3, 11,  4, 12] [ 5, 13,  6, 14] [ 7, 15,  8, 16]
  temp         = _mm_srli_epi16(mtrx, 8);              // [9, 0, 10,  0] [11,  0, 12,  0] [13,  0, 14,  0] [15,  0, 16,  0]
  mtrx         = _mm_and_si128(mtrx, mask);            // [1, 0,  2,  0] [ 3,  0,  4,  0] [ 5,  0,  6,  0] [ 7,  0,  8,  0] 
  mtrx         = _mm_packus_epi16(mtrx, temp);         // [1, 2,  3,  4] [ 5,  6,  7,  8] [ 9, 10, 11, 12] [13, 14, 15, 16]

非常感谢您的解释!我将使用SSE3作为主要路径,并在没有SSE3的平台上回退到SSE2版本。 - born49
@user3809354:没问题!记住你需要在启动时调用CPUID并存储你需要确定要运行哪个版本的代码所需的任何内容。MSDN是一个非常好的起点,可以看到那里有什么。你可以向上导航树以查看SSSE3和SSE4内部函数。我发现其中一些东西真的需要示例(例如洗牌掩码值的含义),但这就是SO的用途。 :) - Apriori
@user3809354:顺便说一下,我意识到在SSE2版本中调用_mm_srli_epi16而不是_mm_srli_si128可以节省几个指令,因为它将每个16位组件都向右移动零。这避免了后面的掩码,因为在调用pack之前,每个16位组件的高字节需要为零。这是因为没有截断版本的pack(即_mm_packs_epi16和_mm_packus_epi16分别执行有符号和无符号饱和),所以要丢弃上字节,它需要包含零。我已更新答案的代码以反映此更改。 - Apriori
再次感谢!因为你的帮助,我已经成功让它运行起来了!另外,我必须说SSE / AVX指令集的“正交性”让我笑了好几次 :) - born49

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接