如何优化这个将输入位转换为单词的简单函数?

时间:2010-06-09 15:42:55

标签: c++ c performance algorithm bit-manipulation

我编写了一个函数,它读取字节的输入缓冲区并产生一个字输出缓冲区,其中每个字对于输入缓冲区的每个ON位可以是0x0081,对于每个OFF位都可以是0x007F。给出输入缓冲区的长度。两个阵列都有足够的物理位置。我还有大约2Kbyte的空闲RAM,我可以用它来查找表格。

现在,我发现这个功能是我在实时应用程序中的瓶颈。它将被频繁调用。您能否提出一种如何优化此功能的方法?我看到一种可能性就是只使用一个缓冲区并进行就地替换。

void inline BitsToWords(int8    *pc_BufIn, 
                        int16   *pw_BufOut, 
                        int32   BufInLen)
{
 int32 i,j,z=0;

 for(i=0; i<BufInLen; i++)
 {
  for(j=0; j<8; j++, z++)
  {
   pw_BufOut[z] = 
                    ( ((pc_BufIn[i] >> (7-j))&0x01) == 1? 
                    0x0081: 0x007f );
  }
 }
}

请不要提供任何库,编译器或CPU /硬件特定的优化,因为它是一个多平台项目。

12 个答案:

答案 0 :(得分:6)

  

我还有大约2Kbyte的可用RAM,我可以用它来查找表

您的查找表可以在编译时放在const数组中,因此它可以在ROM中 - 这是否为您提供了直接4KB表的空间?

如果你能负担4KB的ROM空间,唯一的问题是将表构建为.c文件中的初始化数组 - 但只需要完成一次,你可以编写一个脚本来完成它(这可能有助于确保它是正确的,并且如果您决定该表在将来因某种原因需要更改,也可能有所帮助。)

您必须进行配置以确保从ROM到目标阵列的副本实际上比计算进入目的地的内容更快 - 如果出现以下情况,我不会感到惊讶:

/* untested code - please forgive any bonehead errors */
void inline BitsToWords(int8    *pc_BufIn, 
                        int16   *pw_BufOut, 
                        int32   BufInLen)
{
    while (BufInLen--) {
        unsigned int tmp = *pc_BufIn++;

        *pw_BufOut++ = (tmp & 0x80) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x40) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x20) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x10) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x08) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x04) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x02) ? 0x0081 : 0x007f;
        *pw_BufOut++ = (tmp & 0x01) ? 0x0081 : 0x007f; 
    }
}

最终变得更快。我期望该函数的优化构建将把所有内容都放在寄存器中或编码到指令中,除了每个输入字节的单个读取和每个输出字的单个写入。或者非常接近。

您可以通过一次处理多个输入字节来进一步优化,但是您必须处理对齐问题以及如何处理不是块大小的倍数的输入缓冲区处理。这些问题不是难以处理的问题,但它们确实使事情变得复杂,而且不清楚您可能会期待什么样的改进。

答案 1 :(得分:2)

我假设你不能使用parellellism?


这只是一个猜测 - 你真的需要被一个探查器引导 - 但我认为查找表可以工作。

如果我理解正确,输入数组中的每个字节在输出中产生16个字节。因此,为单字节输入提供16字节输出的查找表应该采用4KiB - 这比您必须的更多。

您可以将每个字节拆分为4位的两部分,这样可以将请求表的大小减小到256字节:

int16[0x0F][4] values = {...};
void inline BitsToWords(int8    *pc_BufIn, int16   *pw_BufOut, int32   BufInLen)
{  
  for(int32 i=0; i<BufInLen; ++i, BufOut+=8)
  {
    memcpy(pw_BufOut,values[pc_BufIn[i]&0x0F]);
    memcpy(pw_BufOut+4,values[(pc_BufIn[i]&0xF0)>>4]);
  }
}

另外,如果您发现循环开销过大,可以使用Duff's Device

答案 2 :(得分:2)

首次尝试:

void inline BitsToWords(int8    *pc_BufIn,  
                        int16   *pw_BufOut,  
                        int32   BufInLen) 
{ 
 int32 i,j=0;
 int8 tmp;
 int16 translate[2] = { 0x007f, 0x0081 };

 for(i=0; i<BufInLen; i++) 
 { 
  tmp = pc_BufIn[i];
  for(j=0x80; j!=0; j>>=1) 
  { 
   *pw_BufOut++ = translate[(tmp & j) != 0];
  } 
 } 
} 

第二次尝试,从Michael Burr(已经获得+1的人)中无耻地偷窃:

void inline BitsToWords(int8    *pc_BufIn,  
                        int16   *pw_BufOut,  
                        int32   BufInLen) 
{ 
    while (BufInLen--) { 
        int16 tmp = *pc_BufIn++; 

        *pw_BufOut++ = 0x007f + ((tmp >> 6) & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp >> 5) & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp >> 4) & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp >> 3) & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp >> 2) & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp >> 1) & 0x02); 
        *pw_BufOut++ = 0x007f + (tmp & 0x02); 
        *pw_BufOut++ = 0x007f + ((tmp << 1) & 0x02);  
    } 
} 

答案 3 :(得分:1)

假设pc_bufInpw_bufOut指向非重叠的内存区域,我可以想到几个优化。首先,您可以将指针声明为非别名:

void inline BitsToWords(int8  * restrict pc_BufIn, 
                        int16 * restrict pw_BufOut, 
                        int32            BufInLen)

这将允许编译器执行否则将不被允许的优化。请注意,您的编译器可能使用不同的关键字;我认为有些使用__restrict__或者可能有特定于编译器的属性。请注意,唯一的要求是pc_bufInpw_bufOut不重叠。这应该可以立即提高性能,因为只要写出pc_bufIn,编译器就不会尝试重新读取pw_bufOut(每8次写入可以节省7次读取)。

如果该关键字不可用,则可以进行替代优化:

{
 char* bufInEnd = pc_bufIn + BufInLen;
 While(pc_bufIn != bufInEnd) {
 {
  char tmp = *pc_bufIn++;
  for(int j=0; j<8; j++)
  {
   *pw_BufOut++ =  ( (tmp & (0x80 >> j) != 0)? 
                    0x0081: 0x007f );
  }
 }
}

上面的轻微重写对我来说更容易理解,因为它明确说明了CPU所采用的路径,但我希望优化是显而易见的:将值pc_bufIn[i]存储到临时局部变量,而不是在内循环的每次迭代中命中指针。

另一个不那么明显的优化将利用大多数CPU上可用的越来越常见的矢量硬件(包括ARM的NEON和Intel的SSE)来一次合成16个字节的结果。我建议调查这个选项。

答案 4 :(得分:1)

如果你想要原始速度,那么使用查找表(避免带有位移的内部循环)可能是最好的方法。

static int16 [] lookup = {
  0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 
  0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x0081, 
  0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x0081, 0x007f, 
  0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x007f, 0x0081, 0x0081,
  /* skip 251 entries */
  0x0081, 0x0081, 0x0081, 0x0081, 0x0081, 0x0081, 0x0081, 0x0081, 
};

void inline BitsToWords(int8 * input, int16 * output, int32 length) {
  while ( length-- ) {
    memcpy( output, lookup[ *input++ ], 16 );
    output += 8; 
  }
}

问题在于查找表本身将是4KB(256 * 16),这比您可用的大。这可以通过两种方式之一解决。最简单和最小的解决方案是这样的:

static int16 [] lookup = {
  0x007f, 0x007f, 0x007f, 0x007f, 
  0x007f, 0x007f, 0x007f, 0x0081, 
  0x007f, 0x007f, 0x0081, 0x007f, 
  0x007f, 0x007f, 0x0081, 0x0081,
  /* skip 11 entries */
  0x0081, 0x0081, 0x0081, 0x0081, 
};

void inline BitsToWords(int8 * input, int16 * output, int32 length) {
  while ( length-- ) {
    int 8 c = *input++;
    memcpy( output, &lookup[ c &0x0f ], 8 );
    memcpy( output+4, &lookup[ c >> 4 ], 8 );
    output += 8; 
  }
}

更复杂但可能更快的方法是使用De Bruijn sequence对所有可能的查找值进行编码。这会将查找表从4KB减少到512 + 14,但需要额外的间接级别和另一个索引表(256字节),总共782个字节。这将删除一个memcpy()调用,以及shift和bitwise,并以一个或多个索引为代价。在您的情况下可能没有必要,但有趣的都是相同的。

答案 5 :(得分:0)

我打算建议一个boost :: for_each,因为它会解开循环,但结尾是未知的。我认为你最好的解决方法是解开内循环。我想方设法做到这一点。在mpl :: range上的boost :: for_each可能是一个选项。

答案 6 :(得分:0)

立刻想到的是:

  • 展开内部循环(编译器可能已经这样做了,但如果您手动执行此操作,则可以进一步优化,请参阅下文)
  • 不要保留“z”,而是保持一个递增的指针(编译器可能已经这样做了)
  • 不是为每个展开的项目执行比较,而是向下移动您提取的班次,使其位于第二位。将其添加到0x7f并将其添加到值中。这将为每个人提供0x7F或0x81。

最好的事情是看看为目标平台生成了什么类型的汇编程序,并查看编译器正在做什么。

编辑:我不会使用查找表。额外缓存未命中的成本可能会超过简单计算的成本。

EDIT2:让我到另一台计算机并启动编译器,我会看到我能做什么。

答案 7 :(得分:0)

您可以将pc_BufIn[i]提取到外部循环中。 乍一看,当在第二个循环中向后计数时,您可以跳过7-j计算。

答案 8 :(得分:0)

我可能会建议创建一个包含8个可能的单位掩码(即0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80)的查找表,然后使用它们与循环中的位域进行比较。伪代码(上面称为bitmask的位掩码,按适当的顺序):

for(i=0,i<BufInLen;i++)
  for(j=0;j<8;j++,z++)
    pw_BufOut[z]=(pc_BufIn[i]&bitmask[j])==0?0x007f:0x0081;

答案 9 :(得分:0)

如果您不介意在内存中有256个pw_Bufout,您可以尝试生成所有可能的输出,并通过将其更改为pw_BufOut [i] = perm [pc_BufIn [i]]来跳过第二个循环; (perm是一个包含所有排列的数组)

答案 10 :(得分:0)

首先,因为你有点笨拙,所以把一切都改成无符号。这消除了由于符号扩展或其他符号相关操作而产生的任何不利影响。

您可以使用修改后的Duff设备:

void inline BitsToWords(int8    *pc_BufIn, 
                        int16   *pw_BufOut, 
                        int32   BufInLen)
{
    uint32 i,j,z=0;

    for(i=0; i<BufInLen; i++)
    {
        uint8   byte = pc_BufIn[i];
        for (j = 0; j < 2; ++j)
        {
            switch (byte & 0x0F)
            {
                case 0:     // 0000 binary
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    break;
                case 1:     // 0001 binary
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x81;
                    break;
                case 2:     // 0010 binary
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x7F;
                    pw_BufOut[z++] = 0x81;
                    pw_BufOut[z++] = 0x7F;
                    break;

               // And so on ...
                case 15:        // 1111 binary
                    pw_BufOut[z++] = 0x81;
                    pw_BufOut[z++] = 0x81;
                    pw_BufOut[z++] = 0x81;
                    pw_BufOut[z++] = 0x81;
                    break;
            } // End: switch
            byte >>= 1;
        }
    }
}

答案 11 :(得分:0)

首先,你是为8段显示做的,不是吗?

你可能想要

#include <stdint.h>

它包含typedef个大小整数,名称为uint8_tuint_fast8_t。您的类型与第一种形式的用途相似,但如果目标处理器与该大小的数据更好地工作,则快速版本可能更大。但是,您可能不希望更改数组类型;主要是你的本地变量类型。

void inline BitsToWords(int8    *pc_BufIn, 
                        int16   *pw_BufOut, 
                        int32   BufInLen)
{
  //int32 i,j,z=0;
  /* This is a place you might want to use a different type, but
   * I don't know for sure.  It depends on your processor, and I
   * didn't use these variables */

  int8 * end = pc_BufIn + BufInLen; /* So that you can do pointer math rather than
                                    * index. */
  while (end < pc_BufIn)
  {
    uint_fast8_t cur = *(pc_BufIn++);
    uint_fast8_t down = 8;

    do
    {
       *(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 ); /* When the bottom bit is set, add 2 */
       /* By doing this with addition we avoid a jump. */

       cur >>= 1; /* next smallest bit */
    } while (--down);
  }
}

在这段代码中,我改变了第二个循环的顺序,倒计时而不是向上计数。如果您的下限为0或-1,这通常会更有效。而且,无论如何,你似乎从最重要的一点开始。

或者,您可以展开内部循环并生成更快的代码并取消down变量。您的编译器可能已经在为您执行此操作。

*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );
cur >>= 1; /* next smallest bit */
*(pw_BufOut++) = 0x07f + ( (mask&cur)<< 1 );

对于外部循环,我将其更改为仅增加指针,而不是使用array[index]和索引测试作为条件。许多处理器实际上可以为您执行pointer+offset,在这些处理器上,pointer++方法可能不适合您。在这种情况下,我建议你可以尝试倒转外循环并倒计时直到index < 0。尝试在测试之前递减它通常会导致将相同的标志设置为显式地将值测试为0,并且编译器通常会在启用优化时利用它。

您可能想要尝试的另一件事是使用比字节更大的块作为输入。您将不得不担心端序问题和非字大小的输入数组。

您可能想要考虑的另一件事是,不是一次对整个可变长度字符串执行此操作。您可以为每个调用执行一个输入字节或一个字,然后将8 * 16块内存传递给其他内容(我假设是一块硬件)。然后,您可以减少输出数组的内存需求,这可能会提高缓存性能。