你会如何转置二进制矩阵?

时间:2015-07-31 09:17:34

标签: c++ math matrix binary transpose

我在C ++中有二进制矩阵,我用8位值的向量重复。

例如,以下矩阵:

1 0 1 0 1 0 1
0 1 1 0 0 1 1
0 0 0 1 1 1 1

表示为:

const uint8_t matrix[] = {
    0b01010101,
    0b00110011,
    0b00001111,
};

我这样做的原因是因为然后计算这样的矩阵和8位向量的乘积变得非常简单和有效(每行只有一个按位AND和奇偶校验计算),这是比单独计算每个位要好得多。

我现在正在寻找一种转换这种矩阵的有效方法,但是我无法在不必手动计算每个位的情况下弄清楚如何做到这一点。

为了澄清,对于上面的例子,我想从转置中得到以下结果:

const uint8_t transposed[] = {
    0b00000000,
    0b00000100,
    0b00000010,
    0b00000110,
    0b00000001,
    0b00000101,
    0b00000011,
    0b00000111,
};

注意:我更倾向于使用任意大小的矩阵来计算这个算法,但我也对只能处理某些大小的算法感兴趣。

8 个答案:

答案 0 :(得分:8)

我花了更多时间寻找解决方案,而且我找到了一些好的解决方案。

SSE2方式

在现代的x86 CPU上,使用SSE2指令可以非常有效地转换二进制矩阵。使用这些指令可以处理16×8矩阵。

这个解决方案受到this blog post by mischasan的启发,远远优于我迄今为止对这个问题的每一个建议。

这个想法很简单:

  • #include <emmintrin.h>
  • 将16 uint8_t个变量打包到__m128i
  • 使用_mm_movemask_epi8获取每个字节的MSB,生成uint16_t
  • 使用_mm_slli_epi64将128位寄存器移位一个
  • 重复,直到你得到所有8 uint16_t s

通用的32位解决方案

不幸的是,我还需要在ARM上完成这项工作。在实现SSE2版本之后,只需找到NEON等价物就很容易,但 Cortex-M CPU(与 Cortex-A 相反)没有SIMD功能,所以NEON目前对我来说并不太有用。

注意:因为 Cortex-M 没有原生的64位算术,所以我无法在任何答案中使用这些想法建议通过将8x8块视为uint64_t来实现。大多数具有 Cortex-M CPU的微控制器也没有太多内存,所以我更喜欢在没有查找表的情况下完成所有这些操作。

经过一番思考后,可以使用普通的32位算术和一些聪明的编码来实现相同的算法。这样,我一次可以使用4×8块。它是由一个同事建议的,神奇之处在于32位乘法的工作原理:你可以找到一个32位的数字,你可以用它来乘法,然后每个字节的MSB在高32位中相互接近。结果。

  • 在32位变量中装入4 uint8_t
  • 屏蔽每个字节的第1位(使用0x80808080
  • 将其与0x02040810
  • 相乘
  • 取乘法
  • 的高32位的4个LSB
  • 通常,您可以屏蔽每个字节中的第N位(将屏蔽右移N位)并乘以幻数,向左移位N位。这里的优点是,如果您的编译器足够智能以展开循环,则掩码和“幻数”都将成为编译时常量,因此移位它们不会产生任何性能损失。最后一个4位系列有一些问题,因为那时会丢失一个LSB​​,因此在这种情况下我需要将输入左移8位并使用与第一个4位系列相同的方法。

如果使用两个4×8块执行此操作,则可以完成8x8块并排列结果位,以便一切都进入正确的位置。

答案 1 :(得分:5)

我的建议是,你不进行换位,而是向矩阵数据中添加一位信息,指示矩阵是否被转置。

现在,如果你想将转置矩阵与向量相乘,它将与向左乘以矩阵>相同(然后转置)。这很简单:只需对8位数字进行xor次操作。

然而,这使得一些其他操作变得复杂(例如,添加两个矩阵)。但是在评论中你说乘法正是你想要优化的。

答案 2 :(得分:4)

我的建议是使用查找表来加速处理。

另外需要注意的是,对于矩阵的当前定义,最大大小为8x8位。这适用于uint64_t,因此我们可以使用它,特别是在使用64位平台时。

我已经找到了一个使用查找表的简单示例,您可以在下面找到并运行:http://www.tutorialspoint.com/compile_cpp11_online.php在线编译器。

示例代码

#include <iostream>
#include <bitset>
#include <stdint.h>
#include <assert.h>

using std::cout;
using std::endl;
using std::bitset;

/* Static lookup table */
static uint64_t lut[256];

/* Helper function to print array */
template<int N>
void print_arr(const uint8_t (&arr)[N]){
    for(int i=0; i < N; ++i){
        cout << bitset<8>(arr[i]) << endl;
    }
}

/* Transpose function */

template<int N>
void transpose_bitmatrix(const uint8_t (&matrix)[N], uint8_t (&transposed)[8]){
    assert(N <= 8);

    uint64_t value = 0;
    for(int i=0; i < N; ++i){
        value = (value << 1) + lut[matrix[i]];
    }

    /* Ensure safe copy to prevent misalignment issues */
    /* Can be removed if input array can be treated as uint64_t directly */
    for(int i=0; i < 8; ++i){
        transposed[i] = (value >> (i * 8)) & 0xFF;
    }
}

/* Calculate lookup table */
void calculate_lut(void){
    /* For all byte values */
    for(uint64_t i = 0; i < 256; ++i){
        auto b = std::bitset<8>(i);
        auto v = std::bitset<64>(0);

        /* For all bits in current byte */
        for(int bit=0; bit < 8; ++bit){
            if(b.test(bit)){
                v.set((7 - bit) * 8);
            }
        }

        lut[i] = v.to_ullong();
    }
}

int main()
{
    calculate_lut();

    const uint8_t matrix[] = {
        0b01010101,
        0b00110011,
        0b00001111,
    };

    uint8_t transposed[8];

    transpose_bitmatrix(matrix, transposed);
    print_arr(transposed);

   return 0;
}

工作原理

您的3x8矩阵将转换为8x3矩阵,以8x8阵列表示。 问题是你要将位,你的“水平”表示转换为垂直表示,分成几个字节。

正如我上面提到的,我们可以利用输出(8x8)总是适合uint64_t的事实。我们将利用这个优势,因为现在我们可以使用uint64_t来编写8字节数组,但我们也可以使用它来添加,xor等,因为我们可以对64位整数执行基本算术运算。

3x8矩阵(输入)中的每个条目都是8位宽,为了优化处理,我们首先生成256个条目查找表(对于每个字节值)。条目本身是一个uint64_t,它将包含一个旋转版本的位。

例如:

  

byte = 0b01001111 = 0x4F
    lut [0x4F] = 0x0001000001010101 =(uint8_t []){0,1,0,0,1,1,1,1}

现在进行计算:

对于计算,我们使用uint64_t,但请记住,在水下它将代表uint8_t [8]数组。我们简单地移动当前值(从0开始),查找我们的第一个字节并将其添加到当前值。

这里的'魔术'是查找表中uint64_t的每个字节都是1或0,所以它只设置最低有效位(每个字节)。移动uint64_t将移动每个字节,只要我们确保我们不会超过8次!我们可以单独对每个字节执行操作。

<强>问题

正如评论中提到的那样:翻译(翻译(M))!= M所以如果你需要这个,你需要做一些额外的工作。

通过直接映射uint64_t而不是uint8_t [8]数组可以提高性能,因为它省略了“安全复制”以防止对齐问题。

答案 3 :(得分:4)

以下是Jay Foad关于快速布尔矩阵的电子邮件 转置:

布尔转置算法的核心是一个函数I#lll call transpose8x8,它转换一个打包在64位字中的8x8布尔矩阵(从MSB到LSB的行主要顺序)。要转置宽度和高度为8的倍数的任何矩形矩阵,将其分解为8x8块,单独转置每个矩阵并将它们存储在输出中的适当位置。要加载8x8块,您必须加载8个单独的字节并将它们移位并将它们转换为64位字。同样存储的东西。

transpose8x8的普通C实现依赖于以下事实:平行于前导对角线的任何对角线上的所有位向上/向下和向左/向右移动相同的距离。例如,正前方对角线上方的所有位必须向左移动一个位置,向下移动一个位置,即在打包的64位字中向右移动7位。这导致了这样的算法:

transpose8x8(word) {

  return
    (word & 0x0100000000000000) >> 49 // top right corner

  | (word & 0x0201000000000000) >> 42

  | ...

  | (word & 0x4020100804020100) >> 7 // just above diagonal

  | (word & 0x8040201008040201) // leading diagonal

  | (word & 0x0080402010080402) << 7 // just below diagonal

  | ...
  | (word & 0x0000000000008040) << 42

  | (word & 0x0000000000000080) << 49; // bottom left corner

}

这比前一个实现快了大约10倍,前一个实现从内存中的源字节中单独复制每个位,并将其合并到内存中的目标字节中。

或者,如果您有PDEP和PEXT指令,则可以实现完美的随机播放,并使用它来执行Hacker's Delight中提到的转置。这明显更快(但我没有时间方便):

shuffle(word) {
    return pdep(word >> 32, 0xaaaaaaaaaaaaaaaa) | pdep(word, 0x5555555555555555);
} // outer perfect shuffle

transpose8x8(word) { return shuffle(shuffle(shuffle(word))); }

POWER的vgbbd指令在一条指令中有效地实现了整个transpose8x8(因为它是一个128位向量指令,它独立地执行两次,在低64位和高64位)。这比普通的C实现提供了大约15%的加速。 (只有15%因为,虽然比特速度快得多,但总体运行时间现在主要是加载8个字节并将它们组装到transpose8x8的参数所需的时间,并取得结果和存储它作为8个单独的字节。)

答案 4 :(得分:3)

我添加了一个新的awnser而不是编辑我原来的awnser以使其更加明显(不幸的是没有评论权)。

在你自己的awnser中,你添加了第一个不存在的额外要求:它必须在ARM Cortex-M上工作

我确实在我的原始awnser中为ARM提出了另一种解决方案,但忽略了它,因为它不是问题的一部分,似乎不在主题(主要是因为C ++标记)。

ARM特定解决方案Cortex-M:

某些或大多数Cortex-M 3/4都有一个位带区域,可以用于您所需要的,它将位扩展为32位字段,该区域可用于执行原子位操作。

如果你把你的阵列放在一个比特区域,它将在比特带区域有一个“爆炸”镜像,你可以在这里对比特本身使用移动操作。如果你创建一个循环,编译器肯定能够展开和优化只是移动操作。

如果你真的想要,你甚至可以设置一个DMA控制器来处理整批转置操作,只需要一点点努力,并完全从cpu中卸载它:)

也许这可能对你有帮助。

答案 5 :(得分:2)

这是我在gitub上发布的内容(mischasan / sse2 / ssebmx.src) 更改INP()和OUT()以使用感应变量可以保存每个IMUL。 AVX256的速度提高了两倍。 AVX512不是一个选项,因为没有_mm512_movemask_epi8()。

#include <stdint.h>
#include <emmintrin.h>

#define INP(x,y) inp[(x)*ncols/8 + (y)/8]
#define OUT(x,y) out[(y)*nrows/8 + (x)/8]

void ssebmx(char const *inp, char *out, int nrows, int ncols)
{
    int rr, cc, i, h;
    union { __m128i x; uint8_t b[16]; } tmp;

    // Do the main body in [16 x 8] blocks:
    for (rr = 0; rr <= nrows - 16; rr += 16)
        for (cc = 0; cc < ncols; cc += 8) {
            for (i = 0; i < 16; ++i)
                tmp.b[i] = INP(rr + i, cc);
            for (i = 8; i--; tmp.x = _mm_slli_epi64(tmp.x, 1))
                *(uint16_t*)&OUT(rr, cc + i) = _mm_movemask_epi8(tmp.x);
        }

    if (rr == nrows) return;

    // The remainder is a row of [8 x 16]* [8 x 8]?

    //  Do the [8 x 16] blocks:
    for (cc = 0; cc <= ncols - 16; cc += 16) {
        for (i = 8; i--;)
            tmp.b[i] = h = *(uint16_t const*)&INP(rr + i, cc),
            tmp.b[i + 8] = h >> 8;
        for (i = 8; i--; tmp.x = _mm_slli_epi64(tmp.x, 1))
            OUT(rr, cc + i) = h = _mm_movemask_epi8(tmp.x),
            OUT(rr, cc + i + 8) = h >> 8;
    }

    if (cc == ncols) return;

    //  Do the remaining [8 x 8] block:
    for (i = 8; i--;)
        tmp.b[i] = INP(rr + i, cc);
    for (i = 8; i--; tmp.x = _mm_slli_epi64(tmp.x, 1))
        OUT(rr, cc + i) = _mm_movemask_epi8(tmp.x);
}

HTH。

答案 6 :(得分:1)

这有点晚了,但我今天偶然发现了这个交汇处。 如果你看看Hacker's Delight,2nd Edition,有几种算法可以有效地转换布尔数组,从第141页开始。

它们非常有效:我的一位同事获得了大约10倍的因素 在X86上,与天真编码相比,加速。

答案 7 :(得分:0)

受 Roberts 回答的启发,可以利用 Arm Neon 中的多项式乘法来分散位 --

inline poly8x16_t mull_lo(poly8x16_t a) {
     auto b = vget_low_p8(a);
     return vreinterpretq_p8_p16(vmull_p8(b,b));
}
inline poly8x16_t mull_hi(poly8x16_t a) {
     auto b = vget_high_p8(a);
     return vreinterpretq_p8_p16(vmull_p8(b,b));
}

auto a = mull_lo(word);
auto b = mull_lo(a), c = mull_hi(a);
auto d = mull_lo(b), e = mull_hi(b);
auto f = mull_lo(c), g = mull_hi(c);

然后可以使用 vsli 将位组合成对。

auto ab = vsli_p8(vget_high_p8(d), vget_low_p8(d), 1);
auto cd = vsli_p8(vget_high_p8(e), vget_low_p8(e), 1);
auto ef = vsli_p8(vget_high_p8(f), vget_low_p8(f), 1);
auto gh = vsli_p8(vget_high_p8(g), vget_low_p8(g), 1);

auto abcd = vsli_p8(ab, cd, 2);
auto efgh = vsli_p8(ef, gh, 2);
return vsli_p8(abcd, efgh, 4);

Clang 优化了这段代码以避免 vmull2 指令,大量使用 ext q0,q0,8vget_high_p8

迭代方法可能不仅速度更快,而且使用更少的寄存器,并且还简化了 2 倍或更多的吞吐量。

// transpose  bits in 2x2 blocks, first 4 rows
//   x = a b|c d|e f|g h      a i|c k|e m|g o   | byte 0
//       i j|k l|m n|o p      b j|d l|f n|h p   | byte 1
//       q r|s t|u v|w x      q A|s C|u E|w G   | byte 2
//       A B|C D|E F|G H      r B|t D|v F|h H   | byte 3 ...
// ----------------------

auto a = (x & 0x00aa00aa00aa00aaull);
auto b = (x & 0x5500550055005500ull);
auto c = (x & 0xaa55aa55aa55aa55ull) | (a << 7) | (b >> 7);

// transpose 2x2 blocks (first 4 rows shown)
//   aa bb cc dd      aa ii cc kk
//   ee ff gg hh   -> ee mm gg oo
//   ii jj kk ll      bb jj dd ll
//   mm nn oo pp      ff nn hh pp

auto d = (c & 0x0000cccc0000ccccull);
auto e = (c & 0x3333000033330000ull);
auto f = (c & 0xcccc3333cccc3333ull) | (d << 14) | (e >> 14);

// Final transpose of 4x4 bit blocks

auto g = (f & 0x00000000f0f0f0f0ull);
auto h = (f & 0x0f0f0f0f00000000ull);
x = (f & 0xf0f0f0f00f0f0f0full) | (g << 28) | (h >> 28);

在 ARM 中,每个步骤现在可以由 3 条指令组成:

auto tmp = vrev16_u8(x);
tmp = vshl_u8(tmp, plus_minus_1); // 0xff01ff01ff01ff01ull
x = vbsl_u8(mask_1, x, tmp);   // 0xaa55aa55aa55aa55ull

tmp = vrev32_u16(x);
tmp = vshl_u16(tmp, plus_minus_2); // 0xfefe0202fefe0202ull
x = vbsl_u8(mask_2, x, tmp);   // 0xcccc3333cccc3333ull

tmp = vrev64_u32(x);
tmp = vshl_u32(tmp, plus_minus_4); // 0xfcfcfcfc04040404ull
x = vbsl_u8(mask_4, x, tmp);   // 0xf0f0f0f00f0f0f0full