Algorithm 二进制矩阵乘法位旋转黑客 摘要

Algorithm 二进制矩阵乘法位旋转黑客 摘要,algorithm,matrix,bit-manipulation,pseudocode,matrix-multiplication,Algorithm,Matrix,Bit Manipulation,Pseudocode,Matrix Multiplication,您好,假设您有两个不同的独立64位二进制矩阵A和T(T本身是一个转置版本,使用转置版本的矩阵可以在乘法过程中对T的行而不是列进行操作,这对于二进制算术来说非常酷)你想把这些矩阵相乘,唯一的事情是矩阵相乘的结果被截断为64位,如果你在某个特定的矩阵单元中得到一个大于1的值,那么得到的矩阵单元将包含1,否则0 例子 问题: 在最有效的问题中,如何以上述方式乘以这些矩阵 附笔 我试图利用二进制和(即&运算符)而不是在单独的位上执行乘法,在这种情况下,我必须为乘法准备数据: ulong u; u =

您好,假设您有两个不同的独立64位二进制矩阵
A
T
T
本身是一个转置版本,使用转置版本的矩阵可以在乘法过程中对
T
的行而不是列进行操作,这对于二进制算术来说非常酷)你想把这些矩阵相乘,唯一的事情是矩阵相乘的结果被截断为64位,如果你在某个特定的矩阵单元中得到一个大于
1
的值,那么得到的矩阵单元将包含
1
,否则
0

例子 问题: 在最有效的问题中,如何以上述方式乘以这些矩阵

附笔 我试图利用二进制
(即
&
运算符)而不是在单独的位上执行乘法,在这种情况下,我必须为乘法准备数据:

ulong u;

u = T & 0xFF;
u = (u << 00) + (u << 08) + (u << 16) + (u << 24)
  + (u << 32) + (u << 40) + (u << 48) + (u << 56);
在上面的示例中,
R
包含
A
位与
u
位相乘的结果,为了获得最终值,我们必须对一行中的所有位进行
求和。请注意,
C列
包含的值等于上面生成的
传统
矩阵乘法的第一列中的值。问题是,在这一步中,我必须对一个单独的位进行操作,我认为这是次优的方法,我一直在寻找一种并行操作的方法,但没有运气,如果有人知道如何将
R
列中的值“展平”和“合并”到生成的64位矩阵中,如果你能给我写几行,我将不胜感激

谢谢,

编辑 非常感谢David Eisenstat,最终的算法如下所示:

var A = ...;
var T = ...; // T == transpose(t), t is original matrix, algorithm works with transposed matrix

var D = 0x8040201008040201UL;

U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D); T = (T << 8) | (T >> 56); D = (D << 8) | (D >> 56);
U = A & T; U |= U >> 1; U |= U >> 2; U |= U >> 4; U &= 0x0101010101010101UL; U = (U << 8) - U; r |= (U & D);

在Mac OS X/Mono下,这是23倍的性能提升,感谢大家如果您正在寻找一种并行执行密集矩阵乘法的方法,请将结果矩阵划分为块并并行计算每个块

我不确定最有效的方法,但这里有一些方法可以尝试。以下指令序列计算乘积A*T'的主对角线。将T和D旋转8位,并重复7次以上的迭代

// uint64_t A, T;
uint64_t D = UINT64_C(0x8040201008040201);
uint64_t P = A & T;
// test whether each byte is nonzero
P |= P >> 1;
P |= P >> 2;
P |= P >> 4;
P &= UINT64_C(0x0101010101010101);
// fill each nonzero byte with ones
P *= 255;  // or P = (P << 8) - P;
// leave only the current diagonal
P &= D;
//uint64\u t A,t;
uint64_t D=uint64_C(0x8040201008040201);
uint64\u t P=A&t;
//测试每个字节是否为非零
P |=P>>1;
P |=P>>2;
P |=P>>4;
P&=UINT64_C(0x0101010101);
//用1填充每个非零字节

P*=255;//或者P=(P

不清楚您使用的数据结构、语言(是的,我知道您说的是“任何语言”),以及您试图优化的内容(速度?内存?)等等。所有这些都可能对您的解决方案产生深远的影响

一些例子:

  • 假设这是C/C++,您的矩阵是内存中的连续位。每一行/列映射到一个UINT8。在这种情况下,将一行与一列相乘可简化为执行8位按位-&,并检查结果是否大于0(无需求和位)。这需要2条处理器指令
  • 如果您被迫执行逐位操作,请使用按位“或”(
    )而不是
    +
    。某些语言可能会延迟计算,在遇到第一个“1”时停止
  • 如果可以使用多线程,则可以加快计算速度

顺便说一句,我假设你有很多矩阵要处理,否则我会使用一个直接的、可读的代码。我的猜测是,即使有很多矩阵,性能上的提高也可以忽略不计。

如果你允许C/C++然后SSE/AVX机器指令再加上内部编译的更低级的构造er函数允许编写更快的代码(根据我所做的一些基准测试,是4x)。您需要使用非标准向量变量(至少由GCC、ICC、CLang支持):

我正在使用一个类,例如

class BMat8 {
    [...]
  private:
    uint64_t _data;
};
然后,下面的代码应该执行您想要的操作

static constexpr epu rothigh { 0, 1, 2, 3, 4, 5, 6, 7,15, 8, 9,10,11,12,13,14};
static constexpr epu rot2    { 6, 7, 0, 1, 2, 3, 4, 5,14,15, 8, 9,10,11,12,13};

inline BMat8 operator*(BMat8 const& tr) const {
  epu x = _mm_set_epi64x(_data, _data);
  epu y = _mm_shuffle_epi8(_mm_set_epi64x(tr._data, tr._data), rothigh);
  epu data {};
  epu diag =  {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,
               0x80,0x01,0x02,0x04,0x08,0x10,0x20,0x40};
  for (int i = 0; i < 4; ++i) {
    data |= ((x & y) != epu {}) & diag;
    y    = _mm_shuffle_epi8(y, rot2);
    diag = _mm_shuffle_epi8(diag, rot2);
  }
  return BMat8(_mm_extract_epi64(data, 0) | _mm_extract_epi64(data, 1));
}
static constepr epu rothigh{0,1,2,3,4,5,6,7,15,8,9,10,11,12,13,14};
静态constexpr epu rot2{6,7,0,1,2,3,4,5,14,15,8,9,10,11,12,13};
内联BMat8运算符*(BMat8常量和tr)常量{
epu x=_mm_set_epi64x(_数据,_数据);
epu y=_mm_shuffle_epi8(_mm_set_epi64x(tr._数据,tr._数据),rothigh);
epu数据{};
epu diag={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,
0x80,0x01,0x02,0x04,0x08,0x10,0x20,0x40};
对于(int i=0;i<4;++i){
数据|=((x&y)!=epu{})和diag;
y=_mm_shuffle_epi8(y,rot2);
diag=_mm_shuffle_epi8(diag,rot2);
}
返回BMat8(_-mm_-extract_-epi64(数据,0)|u-mm_-extract_-epi64(数据,1));
}

特别是,使用128位寄存器,我可以一次进行两次迭代。

使用我在这里介绍的解决方案,在x86-64上可以非常有效地实现严格布尔代数的解决方案:

唯一的区别是,转置矩阵中的数据也需要按列提取,并在每个64位产品之前重新打包到行中。幸运的是,使用BMI2指令进行并行位提取非常简单,可在GCC上通过内置的_pext_u64访问:

uint64_t mul8x8T (uint64_t A, uint64_t B) {

    const uint64_t COL = 0x0101010101010101;

    uint64_t C = 0;

    for (int i=0; i<8; ++i) {
        uint64_t p = COL & (A>>i); // select column
        uint64_t r = torow( COL & (B>>i) );
        C |= (p*r); // use ^ for GF(2) instead
    }
    return C;
}


uint64_t torow (uint64_t c) {
    const uint64_t ROW = 0x00000000000000FF; // mask of the first row
    const uint64_t COL = 0x0101010101010101; // mask of the first column

    // select bits of c in positions marked by COL,
    // and pack them consecutively
    // last 'and' is included for clarity and is not 
    // really necessary 
    return _pext_u64(c, COL) & ROW;
}

还有其他可能的替代实现,使用较低强度的更多操作,其中最快的将取决于目标体系结构。

也许您只应该标记一种语言……简单地考虑或(如果它是一个算法问题)如果问题不是特定于一种语言,那么就不用任何语言标记。将
matrix
标记更改为
algorithm
,对你来说可以吗?哦,你的意思是删除语言标记而改用algorithm?@Lu4是的,那会更好。目前我使用的是C#,但我正在将性能关键部分转移到OpenCL(基本上是C),我不是要求在OpenCL中提供算法,但C/C++/C#或Java中的任何解决方案都会提供。我使用的是64位无符号整数。具有性能最优的算法
00:00:02.4035870
00:00:57.5147150
// uint64_t A, T;
uint64_t D = UINT64_C(0x8040201008040201);
uint64_t P = A & T;
// test whether each byte is nonzero
P |= P >> 1;
P |= P >> 2;
P |= P >> 4;
P &= UINT64_C(0x0101010101010101);
// fill each nonzero byte with ones
P *= 255;  // or P = (P << 8) - P;
// leave only the current diagonal
P &= D;
using epu = uint8_t __attribute__((vector_size(16)));
class BMat8 {
    [...]
  private:
    uint64_t _data;
};
static constexpr epu rothigh { 0, 1, 2, 3, 4, 5, 6, 7,15, 8, 9,10,11,12,13,14};
static constexpr epu rot2    { 6, 7, 0, 1, 2, 3, 4, 5,14,15, 8, 9,10,11,12,13};

inline BMat8 operator*(BMat8 const& tr) const {
  epu x = _mm_set_epi64x(_data, _data);
  epu y = _mm_shuffle_epi8(_mm_set_epi64x(tr._data, tr._data), rothigh);
  epu data {};
  epu diag =  {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,
               0x80,0x01,0x02,0x04,0x08,0x10,0x20,0x40};
  for (int i = 0; i < 4; ++i) {
    data |= ((x & y) != epu {}) & diag;
    y    = _mm_shuffle_epi8(y, rot2);
    diag = _mm_shuffle_epi8(diag, rot2);
  }
  return BMat8(_mm_extract_epi64(data, 0) | _mm_extract_epi64(data, 1));
}
uint64_t mul8x8T (uint64_t A, uint64_t B) {

    const uint64_t COL = 0x0101010101010101;

    uint64_t C = 0;

    for (int i=0; i<8; ++i) {
        uint64_t p = COL & (A>>i); // select column
        uint64_t r = torow( COL & (B>>i) );
        C |= (p*r); // use ^ for GF(2) instead
    }
    return C;
}


uint64_t torow (uint64_t c) {
    const uint64_t ROW = 0x00000000000000FF; // mask of the first row
    const uint64_t COL = 0x0101010101010101; // mask of the first column

    // select bits of c in positions marked by COL,
    // and pack them consecutively
    // last 'and' is included for clarity and is not 
    // really necessary 
    return _pext_u64(c, COL) & ROW;
}
uint64_t torow (uint64_t c) {
    const uint64_t ROW = 0x00000000000000FF; // select 8 lowest consecutive bits to get the first row
    const uint64_t COL = 0x0101010101010101; // select every 8th bit to get the first column
    const uint64_t DIA = 0x8040201008040201; // select every 8+1 bit to obtain a diagonal

    c *= ROW; // "copies" first column to the rest
    c &= DIA; // only use diagonal bits or else there will be position collisions and unexpected carries
    c *= COL; // "scatters" every bit to all rows after itself; the last row will now contain the packed bits
    return c >> 56; // move last row to first & discard the rest
}