假设我们有,并且我们必须找到
z=X[0]*Y[0] + X[1]*Y[1] + X[2]*Y[2] + X[3]*Y[3]
(在这种情况下将给我们-4
)。很显然,X可以用二进制数字表示,例如,对于L=32,使用int32类型的整数。然后,我们所要做的就是找到该整数与包含32个整数的数组的点积。你有什么想法或建议如何非常快地做到这一点吗?
z=X[0]*Y[0] + X[1]*Y[1] + X[2]*Y[2] + X[3]*Y[3]
(在这种情况下将给我们-4
)。这确实需要进行分析,但你可能需要考虑另一种选择:
int result=0;
int mask=1;
for ( int i = 0; i < L; i++ ){
if ( X & mask ){
result+=Y[i];
}
mask <<= 1;
}
正如下面的评论所指出的那样,展开循环可以通过手动或通过编译器标志(例如在GCC上的“-funroll-loops”)来加速此过程(省略循环条件)。
编辑
在下面的评论中,提出了以下良好的调整建议:
int result=0;
for ( int i = 0; i < L; i++ ){
if ( X & 1 ){
result+=Y[i];
}
X >>= 1;
}
result += Y[i] & -(X&1);
- R.. GitHub STOP HELPING ICEresult+=Y[i] * (X & 1);
- The Nomadic Coder试试这个:
int result=0;
for ( int i = 0; i < L; i++ ){
result+=Y[i] & (~(((X>>i)&1)-1));
}
研究一下SSE2是否有帮助?它已经具备点积运算,此外,您还可以轻松地并行执行4(或者可能是8,我忘记了寄存器大小)个简单迭代的原始循环。
SSE还具有一些简单的逻辑操作,因此它可能能够进行加法而不使用任何条件操作... 同样,您需要查看可用的操作。
既然大小明确地不重要,我认为以下代码可能是最有效的通用代码:
int result = 0;
for (size_t i = 0; i < 32; ++i)
result += Y[i] & -X[i];
< p > 二进制编码 < code > X 没有任何实际作用(即使循环可能根据 @Mathieu 的正确说明提前终止)。但是在循环内省略 < code > if 却能有所帮助。< /p>
< p > 当然,像其他人指出的那样,循环展开可以极大地加速此过程。< /p>
这个解决方案与Micheal Aaron的解决方案相同,但稍微快一些(根据我的测试):
long Lev=1;
long Result=0
for (int i=0;i<L;i++) {
if (X & Lev)
Result+=Y[i];
Lev*=2;
}
我认为有一种数学方法可以快速确定一个字中的下一个位,如果您的X数据非常稀疏,这应该会提高性能,但目前我无法找到这种数学公式。
我看到很多人用位运算技巧(避免分支)来回答问题,但在我看来,没有一个人得出了正确的循环。
优化@Goz
的答案:
int result=0;
for (int i = 0, x = X; x > 0; ++i, x>>= 1 )
{
result += Y[i] & -(int)(x & 1);
}
优点:
X>>i
)X
在高位包含0,则循环停止得更早现在,我想知道它是否运行得更快,特别是由于for循环的过早停止可能不像循环展开那样容易(与编译时常量相比较)。
int
的原因?这样做不是有些多余吗?(x & 1
已经是一个 int
。) - Konrad RudolphGoz
中复制了这一部分。@mikera/Goz:我也不确定终止条件,因为我担心它可能会影响循环展开,但在每个循环中进行位移似乎更快 :) - Matthieu M. 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;
}
result = 0;
for(int i = 0; i < L ; i++)
if(X[i]!=0)
result += Y[i];
if (X[i] == 1)
更好在哪里?为什么它不是一个好主意? - Konrad Rudolphif(X[i] == 1)
差劣。
这不是一个好主意,因为在条件语句中除了bool
以外的任何东西都被认为是不好的。 :) - Pratik DeoghareX[i] == 1
呢?从效率的角度来看,这是相同的(!),尽管其他答案完全避免了条件语句,但这将是一个好的答案。 - Konrad Rudolph对于小型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);
}
正如mikera所指出的,这个解决方案可能会有指令缓存成本;如果是这样,使用dot4()函数可能会有所帮助。
进一步更新:这可以与mikera的解决方案相结合:
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上使用gcc 4.3.4的-S -O3选项查看生成的汇编代码,我有点惊讶地发现这个函数在dot32()内自动内联,并带有八个16项跳转表。
但是添加__attribute__((__noinline__))似乎会产生更好看的汇编代码。
另一种变化是在switch语句中使用fall-throughs,但是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个快速指令。一个聪明的编译器可以想象展开其他建议的答案以得到相同的结果。
但我仍在仔细检查。
很可能从主存中加载X
和Y
所花费的时间会占主导地位。如果这是您的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;
}