【问题标题】:Fastest way to transpose 4x4 byte matrix转置 4x4 字节矩阵的最快方法
【发布时间】:2014-09-09 02:28:41
【问题描述】:

我有一个 4x4 字节块,我想使用通用硬件进行转置。换句话说,对于字节 A-P,我正在寻找最有效(就指令数量而言)的方法

A B C D
E F G H
I J K L
M N O P

A E I M
B F J N
C G K O
D H L P

我们可以假设我在内存中有指向 AEIM 的有效指针(这样从 A 读取 32 位将得到包含字节 ABCD 的整数)。

由于大小和数据类型的限制,这不是this question 的重复项。我的矩阵的每一行都可以放入一个 32 位整数,我正在寻找可以使用通用硬件快速执行转置的答案,类似于 SSE 宏 _MM_TRANSPOSE4_PS 的实现。

【问题讨论】:

  • 如果您尝试在 C 中执行此操作而不直接写入专用向量指令,我怀疑最好直接执行此操作。转置只涉及 6 次交换(12 字节读取和 12 字节写入),并且可以高效调度。
  • 了解为什么需要这样做会很有用 - 我知道的大多数矩阵处理库很少真正转置矩阵,它们只是将其标记为转置然后访问 (row, col) 改为 (col,row)。没有记忆需要移动。
  • @RogerRowland 当转置一个 16 byte 矩阵时,您可能会发现任何转置标志在重复访问时的成本都高于转置本身。
  • @RogerRowland,一个有用的字节转置示例是将四个像素从 RGBARGBARGBARGBA 转置为 RRRRGGGGBBBBAAAA。
  • @Zboson Cool,你刚刚为这个答案赢得了另一个支持:-)

标签: c++ c optimization matrix bit-manipulation


【解决方案1】:

让我重新表述一下您的问题:您要求可移植的仅限 C 或 C++ 的解决方案。那么:

void transpose(uint32_t const in[4], uint32_t out[4]) {
  // A B C D    A E I M
  // E F G H    B F J N
  // I J K L    C G K O
  // M N O P    D H L P

  out[0] = in[0] & 0xFF000000U; // A . . .
  out[1] = in[1] & 0x00FF0000U; // . F . .
  out[2] = in[2] & 0x0000FF00U; // . . K .
  out[3] = in[3] & 0x000000FFU; // . . . P

  out[1] |= (in[0] <<  8) & 0xFF000000U; // B F . .
  out[2] |= (in[0] << 16) & 0xFF000000U; // C . K .
  out[3] |= (in[0] << 24);               // D . . P

  out[0] |= (in[1] >>  8) & 0x00FF0000U; // A E . .
  out[2] |= (in[1] <<  8) & 0x00FF0000U; // C G K .
  out[3] |= (in[1] << 16) & 0x00FF0000U; // D H . P

  out[0] |= (in[2] >> 16) & 0x0000FF00U; // A E I .
  out[1] |= (in[2] >>  8) & 0x0000FF00U; // B F J .
  out[3] |= (in[2] <<  8) & 0x0000FF00U; // D H L P

  out[0] |= (in[3] >> 24);               // A E I M
  out[1] |= (in[3] >>  8) & 0x000000FFU; // B F J N
  out[2] |= (in[3] <<  8) & 0x000000FFU; // C G K O
}

我不知道如何以其他方式回答它,因为那时您将依赖特定的编译器以特定方式编译它,等等。

当然,如果可以以某种方式简化这些操作本身,那会有所帮助。所以这是这里进一步追求的唯一途径。到目前为止没有什么特别突出的,但对我来说这是漫长的一天。

到目前为止,成本是 12 个班次、12 个 OR、16 个 AND。如果编译器和平台好的话,可以在 9 个 32 位寄存器中完成。

如果编译器非常难过,或者平台没有桶形移位器,那么一些转换可以帮助颂扬移位和掩码只是字节提取的事实:

void transpose(uint8_t const in[16], uint8_t out[16]) {
  // A B C D    A E I M
  // E F G H    B F J N
  // I J K L    C G K O
  // M N O P    D H L P

  out[0]  = in[0];  // A . . .
  out[1]  = in[4];  // A E . .
  out[2]  = in[8];  // A E I .
  out[3]  = in[12]; // A E I M
  out[4]  = in[1];  // B . . .
  out[5]  = in[5];  // B F . .
  out[6]  = in[9];  // B F J .
  out[7]  = in[13]; // B F J N
  out[8]  = in[2];  // C . . .
  out[9]  = in[6];  // C G . .
  out[10] = in[10]; // C G K .
  out[11] = in[14]; // C G K O
  out[12] = in[3];  // D . . .
  out[13] = in[7];  // D H . .
  out[14] = in[11]; // D H L .
  out[15] = in[15]; // D H L P
}

如果你真的想就地洗牌,那么下面就可以了。

void transpose(uint8_t m[16]) {
  std::swap(m[1], m[4]);
  std::swap(m[2], m[8]);
  std::swap(m[3], m[12]);
  std::swap(m[6], m[9]);
  std::swap(m[7], m[13]);
  std::swap(m[11], m[14]);
}

面向字节的版本很可能在现代平台上产生更糟糕的代码。只有基准才能判断。

【讨论】:

  • +1 教我一个新词。我以前从未听说过barrel shifter(太卡在 HLL 的东西上)。
  • 这可以通过 SSSE3 的一条指令来完成:pshufb(忽略加载和存储)。看我的回答。
  • @Zboson 这个问题没有提供平台,所以对我来说,任何明确绑定到一个平台的东西都超出了答案的范围。很高兴知道,但我并不是有意调用任何特定于平台的内在函数。
  • @KubaOber,我明白了。虽然我不确定我的答案是否超出了 OP 问题的范围,但 OP 要求两件事,但他不能同时拥有它们。你回答了其中一个(可移植性),我想表明如果不违反第一个(12 个班次、12 个 OR、16 个 AND 比一次 shuffle 差很多),就无法实现另一个(效率)。
  • 第一个代码示例的最后两行不正确。你应该分别右移 16 和 8。
【解决方案2】:

不确定速度,但这些都可以。

template<typename T, std::size_t Size>
void Transpose(T (&Data)[Size][Size])
{
    for (int I = 0; I < Size; ++I)
    {
        for (int J = 0; J < I; ++J)
        {
            std::swap(Data[I][J], Data[J][I]);
        }
    }
}

template<typename T, std::size_t Size>
void Transpose(T (&Data)[Size * Size])
{
    for (int I = 0; I < Size; ++I)
    {
        for (int J = 0; J < I; ++J)
        {
            std::swap(Data[I * Size + J], Data[J * Size + I]);
        }
    }
}

【讨论】:

    【解决方案3】:

    如果您接受,则可以在 64 位机器上找到有效的解决方案。 首先将 32 位整数常量分别移动 (0,) 1、2 和 3 个字节 [3 shitfs]。然后屏蔽掉不需要的位并执行逻辑 OR [12 AND 与常数,12 OR]。最后,移回 32 位 [3 shift] 并读出 32 位。

    ABCD
    EFGH
    IJKL
    MNOP
    
    ABCD
     EFGH
      IJKL
       MNOP
    
    A---
     E---
      I---
       MNOP
    =======
    AEIMNOP
    AEIM
    
    AB--
     -F--
      -J--
       -NOP
    =======
    ABFJNOP
    BFJN
    
    ABC-
     --G-
      --K-
       --OP
    =======
    ABCGKOP
    CGKO
    
    ABCD
     ---H
      ---L
       ---P
    =======
    ABCDHLP
    DHLP
    

    【讨论】:

      【解决方案4】:

      您需要可饮用性和效率。那么你不能同时拥有它。你说你想用最少的指令来做到这一点。好吧,使用 x86 指令集中的 pshufb 指令(见下文),只需一条 SSE3 指令就可以做到这一点。

      也许 ARM Neon 也有类似的东西。如果您想要效率(并且确定您需要它),那么请学习硬件。

      对于字节,_MM_TRANSPOSE4_PS 的 SSE 等效项是使用带有掩码的 _mm_shuffle_epi8(pshufb 的内在函数)。在主循环之外定义掩码。

      //use -msse3 with GCC or /arch:SSE2 with MSVC
      #include <stdio.h>
      #include <tmmintrin.h> //SSSE3
      int main() {
          char x[] = {0,1,2,3, 4,5,6,7, 8,9,10,11, 12,13,15,16};
          __m128i mask = _mm_setr_epi8(0x0,0x04,0x08,0x0c, 0x01,0x05,0x09,0x0d, 0x02,0x06,0x0a,0x0e, 0x03,0x07,0x0b,0x0f);
      
          __m128i v = _mm_loadu_si128((__m128i*)x);
          v = _mm_shuffle_epi8(v,mask);
          _mm_storeu_si128((__m128i*)x,v);
          for(int i=0; i<16; i++) printf("%d ", x[i]); printf("\n");
          //output: 0 4 8 12 1 5 9 13 2 6 10 15 3 7 11 16   
      }
      

      【讨论】:

      • 使用_mm_lddqu_si128() 在某些情况下会表现得更好,但绝不会表现得更差。如果 x[] 是 16 字节对齐的,那么使用 _mm_load_si128_mm_store_si128 可能会更快。
      • @St0fF,谢谢!直到现在我才听说过_mm_lddqu_si128()。很有趣。
      【解决方案5】:

      不久前我为 SSE here 发布了同样问题的答案。

      唯一需要添加的是矢量化加载/存储操作。

      这个答案类似于Z boson's answer to this question。可以在那里看到加载/存储的示例。这个答案不同,因为除了 SSE3 实现之外,还有一个 SSE2 实现可以保证在任何 x64 处理器上运行。

      值得注意的是,这两种解决方案都假设整个矩阵在内存中主要是行,但 OP 的问题指出,每一行都可以有自己的指针,这意味着数组可能是碎片化的。

      【讨论】:

        猜你喜欢
        • 2010-11-12
        • 1970-01-01
        • 2013-05-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多