C++ Fast dot产品适用于非常特殊的情况

C++ Fast dot产品适用于非常特殊的情况,c++,c,algorithm,math,C++,C,Algorithm,Math,给定一个大小为L的向量X,其中X的每个标量元素都来自一个二元集合{0,1},如果大小为L的向量Y由整数值元素组成,则需要找到点积z=点(X,Y)。我建议,必须有一个非常快速的方法来做到这一点 假设我们有L=4;X[L]={1,0,0,1};Y[L]={-4,2,1,0}我们必须找到z=X[0]*Y[0]+X[1]*Y[1]+X[2]*Y[2]+X[3]*Y[3](在这种情况下,我们将得到-4) 很明显,X可以用二进制数字表示,例如L=32的整数类型int32。然后,我们要做的就是找到这个整数与3

给定一个大小为L的向量X,其中X的每个标量元素都来自一个二元集合{0,1},如果大小为L的向量Y由整数值元素组成,则需要找到点积z=点(X,Y)。我建议,必须有一个非常快速的方法来做到这一点

假设我们有
L=4;X[L]={1,0,0,1};Y[L]={-4,2,1,0}
我们必须找到
z=X[0]*Y[0]+X[1]*Y[1]+X[2]*Y[2]+X[3]*Y[3]
(在这种情况下,我们将得到
-4


很明显,X可以用二进制数字表示,例如L=32的整数类型int32。然后,我们要做的就是找到这个整数与32个整数数组的点积。您有什么想法或建议如何快速完成吗?

这确实需要分析,但您可能需要考虑另一种选择:

int result=0;
int mask=1;
for ( int i = 0; i < L; i++ ){
    if ( X & mask ){
        result+=Y[i];
    }
    mask <<= 1;
}
int结果=0;
int-mask=1;
对于(int i=0;i
研究SSE2的建议有用吗?它已经有了点积类型的操作,加上您可以简单地并行执行4次(或者8次,我忘了寄存器大小)简单的朴素循环迭代。
SSE也有一些简单的逻辑类型操作,因此它可以在不使用任何条件操作的情况下进行加法而不是乘法。。。同样,您必须查看哪些操作可用。

您可以将位向量存储为一个整数序列,其中每个整数将一对系数打包为位。然后,分量乘法等价于位和。这样,您只需计算设置位的数量,可以这样做:

inline int count(uint32_t x) {
    // see link
}

int dot(uint32_t a, uint32_t b) {
    return count(a & b);
}
有关计数设定位的bit hack,请参阅


编辑:抱歉,我刚刚意识到只有一个向量包含{0,1}元素,而另一个不包含。这个答案只适用于两个向量都被限制为{0,1}集合中的系数的情况。

如果它是1,那么你希望所有比特都通过,如果它是0,那么就没有比特通过。因此,您希望以某种方式将1转换为-1(即0xFFFFFF),并且0保持不变。那只是-X。。。。所以你

Y & (-X)
对于每个元素。。。工作完成了吗

Edit2:要给出一个代码示例,您可以这样做并避免分支:

int result=0;
for ( int i = 0; i < L; i++ )
{
   result+=Y[i] & -(int)((X >> i) & 1);
}
int结果=0;
对于(int i=0;i>i)和1);
}
当然,最好将1和0保持在整数数组中,从而避免移位

编辑:同样值得注意的是,如果Y中的值的大小为16位,那么每个操作可以执行其中2个和操作(如果您有64位寄存器,则为4个)。不过,这确实意味着将X值1乘以1求反为一个更大的整数

ie YVals=-4,16位中的3=0xFFFC,0x3。。。输入1 32位,得到0xFFFC0003。如果将1,0作为X VAL,那么将0xFFFF0000和2组成一个位掩码,得到2个结果,即1按位and运算

另一编辑:

如果你想知道如何使用第二种方法的代码,像这样的代码应该可以工作(尽管它利用了未指定的行为,所以它可能不适用于每一个编译器..适用于我遇到的每一个编译器)

union int1632
{
int32_t i32;
int16_t i16[2];
};
int结果=0;
对于(int i=0;i<(L&~0x1);i+=2)
{
INT3264Y3264;
y3264.i16[0]=Y[i+0];
y3264.i16[1]=Y[i+1];
INT3264X3264;
x326.i16[0]=-(int16_t)((X>>(i+0))&1);
x326.i16[1]=-(int16_t)((X>>(i+1))&1);
int3264 res3264;
res3264.i32=y3264.i32&x3264.i32;
结果+=res3264.i16[0]+res3264.i16[1];
}
如果(i>i)和1);

希望编译器能优化赋值(我不确定,但我的想法可以重新设计,这样它们肯定会优化),并给你一个小的加速,因为你现在只需要按1位进行运算,而不是按2位进行运算。速度会很小,但…

此解决方案与Micheal Aaron的相同,但比Micheal Aaron的略快(根据我的测试):

long Lev=1;
long Result=0
for (int i=0;i<L;i++) {
  if (X & Lev)
     Result+=Y[i];
  Lev*=2;
}
long Lev=1;
长结果=0

对于(inti=0;iRepresente
X
,使用
X[i]=1的位置的链接列表。

要找到所需金额,您需要
O(N)
操作,其中
N
是列表的大小。

这个问题可能没有一般的答案。您需要在所有不同的目标下分析代码。性能将取决于编译器优化,例如大多数现代CPU上可用的循环展开和SIMD指令(x86、PPC和ARM都有自己的实现)。

试试这个:

int result=0;
for ( int i = 0; i < L; i++ ){
    result+=Y[i] & (~(((X>>i)&1)-1));
}
int结果=0;
对于(int i=0;i>i)&1)-1);
}

这避免了条件语句,并使用逐位运算符将标量值屏蔽为0或1。

我看到过许多采用位欺骗(以避免分支)的响应,但没有一个正确的循环:/

优化
@Goz
答案:

int result=0;
for (int i = 0, x = X; x > 0; ++i, x>>= 1 )
{
   result += Y[i] & -(int)(x & 1);
}
优点:

  • 无需每次执行
    i
    位移位操作(
    X>>i
  • 如果
    X
    的高位包含0,则循环会更快停止
现在,我想知道它是否运行得更快,特别是因为for循环的过早停止可能不像编译时常量那样容易展开循环; 对于(int i=0;i
由于大小并不重要,我认为以下可能是最有效的通用代码:

int result = 0;
for (size_t i = 0; i < 32; ++i)
    result += Y[i] & -X[i];
int结果=0;
对于(尺寸i=0;i<32;++i)
结果+=Y[i]&-X[i];
位编码
X
不会给表带来任何东西(即使循环可能会像@Mathieu正确指出的那样提前终止)。但是在循环中省略
if
会带来什么

当然,正如其他人所指出的,循环展开可以大大加快速度int result=0; for (int i = 0, x = X; x > 0; ++i, x>>= 1 ) { result += Y[i] & -(int)(x & 1); }
result = 0;
for(int i = 0; i < L ; i++)
    if(X[i]!=0)
      result += Y[i];
int result = 0;
for (size_t i = 0; i < 32; ++i)
    result += Y[i] & -X[i];
int dot8(unsigned int X, const int Y[])
{
    switch (X)
    {
       case 0: return 0;
       case 1: return Y[0];
       case 2: return Y[1];
       case 3: return Y[0]+Y[1];
       // ...
       case 255: return Y[0]+Y[1]+Y[2]+Y[3]+Y[4]+Y[5]+Y[6]+Y[7];
    }
    assert(0 && "X too big");
}   
int dot32(unsigned int X, const int Y[])
{
    return dot8(X >> 0  & 255, Y + 0)  +
           dot8(X >> 8  & 255, Y + 8)  +
           dot8(X >> 16 & 255, Y + 16) +
           dot8(X >> 24 & 255, Y + 24);
}
static int dot4(unsigned int X, const int Y[])
{
    switch (X)
    {
        case 0: return 0;
        case 1: return Y[0];
        case 2: return Y[1];
        case 3: return Y[0]+Y[1];
        //...
        case 15: return Y[0]+Y[1]+Y[2]+Y[3];
    }
}
int dot(unsigned int X, const int Y[])
{
    return (Y[0] & -!!(X & 1<<0)) +
           (Y[1] & -!!(X & 1<<1)) +
           (Y[2] & -!!(X & 1<<2)) +
           (Y[3] & -!!(X & 1<<3)) +
           //...
           (Y[31] & -!!(X & 1<<31));
}
    int result=0;

    for ( int x=X; x!=0; x>>=4 ){
        switch (x&15) {
            case 0: break;
            case 1: result+=Y[0]; break;
            case 2: result+=Y[1]; break;
            case 3: result+=Y[0]+Y[1]; break;
            case 4: result+=Y[2]; break;
            case 5: result+=Y[0]+Y[2]; break;
            case 6: result+=Y[1]+Y[2]; break;
            case 7: result+=Y[0]+Y[1]+Y[2]; break;
            case 8: result+=Y[3]; break;
            case 9: result+=Y[0]+Y[3]; break;
            case 10: result+=Y[1]+Y[3]; break;
            case 11: result+=Y[0]+Y[1]+Y[3]; break;
            case 12: result+=Y[2]+Y[3]; break;
            case 13: result+=Y[0]+Y[2]+Y[3]; break;
            case 14: result+=Y[1]+Y[2]+Y[3]; break;
            case 15: result+=Y[0]+Y[1]+Y[2]+Y[3]; break;
        }
        Y+=4;
    }
template<int I> inline void calcZ(int (&X)[L], int(&Y)[L], int &Z) {
  Z += X[I] * Y[I]; // Essentially free, as it operates in parallel with loads.
  calcZ<I-1>(X,Y,Z);
}
template< > inline void calcZ<0>(int (&X)[L], int(&Y)[L], int &Z) {
  Z += X[0] * Y[0];
}
inline int calcZ(int (&X)[L], int(&Y)[L]) {
    int Z = 0;
    calcZ<L-1>(X,Y,Z);
    return Z;
}