快速点产品,适用于非常特殊的情况

时间:2010-05-13 10:00:02

标签: c++ c algorithm math

给定大小为L的向量X,其中X的每个标量元素来自二进制集{0,1},如果大小为L的向量Y包含,则找到点乘积z = dot(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个整数的数组。您是否有任何想法或建议如何快速完成?

14 个答案:

答案 0 :(得分:7)

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

int result=0;
int mask=1;
for ( int i = 0; i < L; i++ ){
    if ( X & mask ){
        result+=Y[i];
    }
    mask <<= 1;
}

通常位移和按位运算比乘法更快,但if语句可能比乘法慢,但是对于分支预测和大L我的猜测是它可能更快。但是,您确实需要对其进行分析,以确定它是否会导致任何加速。

正如下面的评论中指出的那样,手动或通过编译器标志(例如GCC上的“-funroll-loops”)展开循环也可以加快这一速度(忽略循环条件)。

修改
在下面的评论中,提出了以下好的调整:

int result=0;
for ( int i = 0; i < L; i++ ){
    if ( X & 1 ){
        result+=Y[i];
    }
    X >>= 1;
}

答案 1 :(得分:4)

建议调查SSE2有帮助吗?它已经具有点积类型操作,而且您可以平行地执行4个(或者可能是8个,我忘记了寄存器大小)并行的简单迭代。 SSE还有一些简单的逻辑类型操作,因此它可以在不使用任何条件操作的情况下进行加法而不是乘法...再次,您必须查看可用的操作。

答案 2 :(得分:4)

试试这个:

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

这避免了条件语句,并使用按位运算符用零或1来掩盖标量值。

答案 3 :(得分:3)

由于明确的大小不重要,我认为以下可能是最有效的通用代码:

int result = 0;
for (size_t i = 0; i < 32; ++i)
    result += Y[i] & -X[i];

比特编码X只是没有给表带来任何东西(即使循环可能会在@Mathieu正确注意时提前终止)。但是忽略循环中的if

当然,正如其他人所指出的那样,循环展开可以大大加快这一速度。

答案 4 :(得分:2)

这个解决方案与Micheal Aaron的解决方案相同,但速度稍快(通过我的测试):

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

我认为有一种数字方法可以快速建立单词中的下一个设置位,如果你的X数据非常稀疏但目前找不到所说的数字公式,那么这应该可以提高性能。

答案 5 :(得分:2)

我已经看到了一些有点诡计的回复(为了避免分支)但没有一个得到正确的循环imho:/

优化@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循环的过早停止可能不像循环展开那么容易(与编译时常量相比)。

答案 6 :(得分:2)

如何将移位循环与小型查找表相结合?

    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;
    }

这个性能取决于编译器在优化switch语句方面有多好,但根据我的经验,他们现在非常擅长......

答案 7 :(得分:1)

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

答案 8 :(得分:1)

result = 0;
for(int i = 0; i < L ; i++)
    if(X[i]!=0)
      result += Y[i];

答案 9 :(得分:1)

对于 L,您可以使用switch语句而不是循环语句。例如,如果L = 8,您可以:

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");
}   

如果L = 32,你可以编写一个dot32()函数,它调用dot8() 次,如果可能,内联。 (如果编译器拒绝内联dot8(),则可以将dot8()重写为宏以强制内联。)已添加

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);
}

正如迈克拉指出的那样,这个解决方案可能会有一个指令缓存成本;如果是这样,使用 dot4 ()函数可能会有所帮助。

进一步更新:这可以与迈克拉的解决方案结合使用:

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];
    }
}

在CYGWIN上查看带有 4.3.4的-S -O3选项的结果汇编程序代码,我有点惊讶地发现它在dot32()中自动内联,带有< strong> 八个 16个条目的跳转表。

但是添加__attribute __((__ noinline__))似乎会产生更好看的汇编程序。

另一种变体是在switch语句中使用fall-through,但是 gcc 会添加jmp指令,并且它看起来不会更快。

编辑 - 完全新答案: 在考虑了Ants Aasma提到的100周期惩罚以及其他答案之后,上述情况可能并非最佳。相反,您可以 手动 展开循环,如下所示:

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));
}

这在我的机器上生成32 x 5 = 160快速指令。可以想象,智能编译器可以展开其他建议的答案以得到相同的结果。

但我仍在仔细检查。

答案 10 :(得分:1)

从主内存加载XY所花费的时间很可能会占主导地位。如果您的CPU架构就是这种情况,那么加载较少时算法会更快。这意味着将X存储为位掩码并将其扩展到L1缓存将加速整个算法。

另一个相关问题是您的编译器是否会为Y生成最佳负载。这是高度CPU和编译器依赖。但总的来说,如果编译器能够准确地看到何时需要哪些值,这会有所帮助。您可以手动展开循环。但是,如果L是一个常数,请将其留给编译器:

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;
}

(Konrad Rudolph在评论中质疑这一点,想知道内存使用。这不是现代计算机体系结构的真正瓶颈,内存和CPU之间的带宽是。如果Y是某种方式,这个答案几乎无关紧要已经在缓存中。)

答案 11 :(得分:0)

您可以将位向量存储为一个整数序列,其中每个int将一些系数打包为位。然后,分量乘法相当于bit-and。有了这个,您只需要计算可以这样做的设置位数:

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

int dot(uint32_t a, uint32_t b) {
    return count(a & b);
}

有点计算设置位,请参阅http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel

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

答案 12 :(得分:0)

嗯,你希望所有的比特都是过去的,如果它是1,没有,如果它是0.所以你想以某种方式将1转为-1(即0xffffffff),0保持不变。那只是-X ......所以你做......

Y & (-X)

每个元素......完成工作?

Edit2:为了给出一个代码示例,您可以执行类似的操作并避免分支:

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

当然,你最好将1和0保持在一组整数中,从而避免轮班。

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

即YVals = -4,3位在16位= 0xFFFC,0x3 ...放入1 32位,你得到0xFFFC0003。如果你有1个0作为X值,那么你形成一个0xFFFF0000的位掩码和2个一起,你有2个结果,1个按位 - 和op。

另一个编辑:

如果你想要代码如何做第二种方法喜欢这应该工作(虽然它利用了未指定的行为,所以它可能不适用于每个编译器..适用于每个编译器我但是我遇到了。)

union int1632
{
     int32_t i32;
     int16_t i16[2];
};

int result=0;
for ( int i = 0; i < (L & ~0x1); i += 2 )
{
    int3264 y3264;
    y3264.i16[0] = Y[i + 0];
    y3264.i16[1] = Y[i + 1];

    int3264 x3264;
    x3264.i16[0] = -(int16_t)((X >> (i + 0)) & 1);
    x3264.i16[1] = -(int16_t)((X >> (i + 1)) & 1);

    int3264 res3264;
    res3264.i32  = y3264.i32 & x3264.i32;

    result += res3264.i16[0] + res3264.i16[1];    
}

if ( i < L )
    result+=Y[i] & -(int)((X >> i) & 1);

希望编译器能够优化分配(在我的头脑中我不确定,但这个想法可以重新工作,以便它们肯定是)并且给你一个小的加速,你现在只需要按位进行1次而不是2次。虽然速度很快......

答案 13 :(得分:0)

使用X所在地的链接列表代表x[i] = 1。 要查找所需的金额,您需要O(N)这些操作,其中N是您列表的大小。