如何优化 C 代码:寻找下一个设置位并找到相应数组元素的总和

Posted

技术标签:

【中文标题】如何优化 C 代码:寻找下一个设置位并找到相应数组元素的总和【英文标题】:How to optimize C code : looking for the next set bit and finding sum of corresponding array elements 【发布时间】:2015-06-09 20:24:32 【问题描述】:

编辑:现在我意识到我没有很好地解释我的算法。我会再试一次。

我正在做的事情与两个向量的点积非常相似,但有区别。我有两个向量:一个位向量和一个相同长度的浮点向量。所以我需要计算总和: float[0]*bit[0]+float[1]*bit[1]+..+float[N-1]*bit[N-1],但与经典点积的区别在于我需要在每个设置位之后跳过一些固定数量的元素。

例子:

vector of floats = 1.5, 2.0, 3.0, 4.5, 1.0
vector of bits   = 1, 0, 1, 0, 1 
nSkip = 2

在这种情况下,总和计算如下:

sum = floats[0]*bits[0]
bits[0] == 1, so skipping 2 elements (at positions 1 and 2)
sum = sum + floats[3]*bits[3]
bits[3] == 0, so no skipping
sum = sum + floats[4]*bits[4]
result = 1.5*1+4.5*0+1.0*1 = 2.5

以下代码使用不同的数据多次调用,因此我需要优化它以在我的 Core i7 上尽可能快地运行(我不太关心与其他任何东西的兼容性)。它在一定程度上进行了优化,但仍然很慢,但我不知道如何进一步改进它。 位数组实现为 64 位无符号整数数组,它允许我使用 bitscanforward 来查找下一个设置位。

代码:

unsigned int i = 0;
float fSum = 0;
do

  unsigned int nAddr = i / 64;
  unsigned int nShift = i & 63;
  unsigned __int64 v = bitarray[nAddr] >> nShift;
  unsigned long idx;
  if (!_BitScanForward64(&idx, v))
  
    i+=64-nShift; 
    continue;
  
  i+= idx;
  fSum  += floatarray[i];
  i+= nSkip;
   while(i<nEnd);

Profiler 显示 3 个最慢的热点:

1. v = bitarray[nAddr] >> nShift (memory access with shift)
2. _BitScanForward64(&idx, v) 
3. fSum += floatarray[i]; (memory access)

但可能有不同的方法可以做到这一点。我正在考虑在位向量中的每个设置位之后重置 nSkip 位,然后计算经典的点积 - 还没有尝试,但老实说不要相信它会通过更多的内存访问更快。

【问题讨论】:

我的回答对你有帮助吗? nategoose,是的,谢谢:您关于内存访问的想法很有帮助 - 我能够通过使用简单的“缓存”来避免重新读取相同的 64 位字 (bitarray[nAddr])之前的值并获得了大约 20% 的加速。 nEnd 是否总是 64 的整数倍? 根据数据的大小,您可以将其分成多个线程并同时执行,然后重新组合。 【参考方案1】:

你在循环中有太多的操作。您也只有一个循环,因此对于每个标志字(64 位无符号整数)确实需要发生的许多操作要额外发生 63 次。

考虑除法是一项昂贵的操作,并在优化代码以提高性能时尽量不要经常这样做。

就需要多长时间而言,内存访问也被认为是昂贵的,因此这也应仅限于必需的访问。

允许您提前退出的测试通常很有用(尽管有时测试本身相对于您要避免的操作而言代价高昂,但这里可能并非如此。

使用嵌套循环应该可以大大简化这一点。外循环应该工作在 64 位字级别,内循环应该工作在位级别。


我注意到我之前的建议中有一个错误。由于这里的除法是 64,即 2 的幂,这实际上并不是一个昂贵的操作,但我们仍然需要尽可能多地远离循环。

/* this is completely untested, but incorporates the optimizations
   that I outlined as well as a few others.
   I process the arrays backwards, which allows for elimination of
   comparisons of variables against other variables, which is much
   slower than comparisons of variables against 0, which is essentially
   free on many processors when you have just operated or loaded the
   value to a register.
   Going backwards at the bit level also allows for the possibility that
   the compiler will take advantage of the comparison of the top bit
   being the same as test for negative, which is cheap and mostly free
   for all but the first time through the inner loop (for each time
   through the outer loop.
 */
double acc = 0.0;

unsigned i_end = nEnd-1;
unsigned i_bit;
int i_word_end;

if (i_end == 0)

     return acc;

i_bit = i_end % 64;
i_word = i_end / 64;

do

    unsigned __int64 v = bitarray[i_word_end];
    unsigned i_upper = i_word_end << 64;
    while (v)
    
         if (v & 0x80000000000000)
         
              // The following code is semantically the same as
              // unsigned i = i_bit_end + (i_word_end * sizeof(v));
              unsigned i = i_bit_end | i_upper;
              acc += floatarray[i];
         
         v <<= 1;
         i--;
     
     i_bit_end = 63;
     i_word_end--;
 while (i_word_end >= 0);

【讨论】:

【参考方案2】:

我认为你应该先检查“如何提问”。您不会因此获得太多支持,因为您要求我们为您完成这项工作,而不是引入特定问题。

我不明白为什么你在两个地方增加同一个变量而不是一个 (i)。 还认为您应该只声​​明一次变量,而不是在每次迭代中。

【讨论】:

很遗憾,我不能在这里介绍一个特定的问题。我有一个非常具体的问题,这是我能代表它的最简单的方式。只是寻找可以提高此任务性能的任何东西。至于增加 i 的值:首先,我添加在 BitScanForward 中找到的值以获取下一个设置位的索引。使用此索引后,我需要第二个增量来跳过下一个 nOffset 元素。

以上是关于如何优化 C 代码:寻找下一个设置位并找到相应数组元素的总和的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode 162. 寻找峰值c++/java详细题解

从一个寄存器读取某些位并写入另一个寄存器的某些位

vs2010怎么把生成的exe与依赖的dll放在不同文件夹下

如何找到现有数组的下一个数字索引?

如何找到现有数组的下一个数字索引?

如何优化a [i] = -b [i] *(c [i] + d);