【问题标题】:How to optimize this simple function which translates input bits into words?如何优化这个将输入位转换为单词的简单函数?
【发布时间】:2010-06-09 15:42:55
【问题描述】:

我编写了一个函数,它读取字节的输入缓冲区并生成字的输出缓冲区,其中每个字可以是输入缓冲区的每个 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/硬件特定的优化,因为它是一个多平台项目。

【问题讨论】:

  • 你是怎么发现这个功能是瓶颈的?你用的是什么分析器?
  • @Sam:我没有使用任何分析器。问题是这个函数会在内部循环中被非常频繁地调用。
  • 如果你没有使用过profiler,你不知道它是一个瓶颈。众所周知,人们不善于发现热点。此外,您是否知道实际的性能问题? “实时”意味着程序必须满足性能限制,而不是一切都必须尽可能快。
  • 您可能会发现最快的代码将取决于平台。
  • @David Thornley:人们通常不擅长寻找热点,但在很多情况下并非所有人都如此,在某些情况下,大多数程序员只要在寻找热点就可以找到热点。以我的经验,如果编译器知道它们是持续操作,那么最难发现的热点涉及可能被提升到循环之外的东西。像while( x &lt; func(my_str) ) 这样的东西 my_str 没有改变,但编译器不知道或不知道func 是纯的——func 变得更热,但对所有程序员来说可能并不明显。

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


【解决方案1】:

我还有大约 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; 
    }
}

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

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

【讨论】:

  • 你的 tmp 必须是 int8 ,而不是 unsigned int。
【解决方案2】:

我假设你不能使用并行?


这只是一个猜测 - 您确实需要由分析器指导 - 但我认为查找表可以工作。

如果我理解正确,输入数组中的每个字节都会在输出中产生 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

【讨论】:

  • 您应该避免使用 Duff 的设备。是的,这是一个可爱的聪明技巧,但很容易搞砸,而且在现代硬件上它不会比 memcpy() 更快,因为 memcpy() 将被优化以利用任何硬件块移动/复制指令.如果没有任何专门的说明,那么我希望 memcpy() 使用 Duff 的设备本身。
  • 从问题中列出的限制来看,可以合理地假设它将在嵌入式平台或较旧的硬件上运行 - 在这种情况下,Duff 的设备可能有意义。
【解决方案3】:

第一次尝试:

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

【讨论】:

  • 通过删除内部循环(以与 Neil 相同的方式),我很想知道使用数组访问与三元运算符形式的比较?
  • 如果您的查找表适合您的处理器的缓存行,那么它肯定会更快;如果不是,那么它可能仍然会更快,因为三元形式仍然只是 if-then-else 语句的捷径。
  • 很大程度上取决于您的处理器。我知道 x86 有一条指令可以将 !=0 直接转换为 (0,1);我不确定其他架构。此外,结果需要乘以 2 来索引数组内存,x86 也是直接这样做的。
【解决方案4】:

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

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

这将允许编译器执行原本不允许的优化。请注意,您的编译器可能使用不同的关键字;我认为有些人使用__restrict__ 或者可能具有特定于编译器的属性。请注意,唯一的要求是 pc_bufInpw_bufOut 不重叠。这应该可以立即提高性能,因为无论何时写出 pw_bufOut,编译器都不会尝试重新读取 pc_bufIn(每 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 个字节的结果。我建议调查该选项。

【讨论】:

  • 我忽略了使用新的 restrict 关键字。 glibc 定义了__restrict 以在restrict 不可用时使用,但我认为如果restrict 不可用,它什么也不做。
  • 如果restrict 不可用,要么它是一个非常旧的编译器(升级!),要么可能有一个属性或#pragma 你可以滥用。
【解决方案5】:

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

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() 调用,以及移位和按位,并以增加一个索引为代价。在你的情况下可能没有必要,但同样有趣。

【讨论】:

  • 您可以使用int8 将查找表的大小减半,但代价是需要使用8 个分配而不是memcpy()
  • 实际上,除非目标内存已经初始化(不一定是有效的假设),否则您需要进行 8 次复制和 8 次分配(零 x+0,复制 x+1,零 x+2 , 复制 x+3, ...)。
【解决方案6】:

我打算建议使用 boost::for_each,因为它会解开循环,但不知道结束。我认为你会得到的最好的结果是解开内部循环。我会想办法做到这一点。 boost::for_each 在 mpl::range 上可能是一个选项。

【讨论】:

    【解决方案7】:

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

    【讨论】:

      【解决方案8】:

      我可能会建议创建 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】:

        首先,因为你有点玩弄,把所有东西都改成无符号的。这消除了由于符号扩展或其他符号相关操作造成的任何不利影响。

        您可以使用修改后的 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;
                }
            }
        }
        

        【讨论】:

        • 这甚至更慢,因为现在您需要与分支进行比较,而不是内部循环中的简单“是/否”分支。顺便说一句,那不是 duff 的设备。
        • 其实在OP的循环中,每个字节有8个比较,或者每个字节至少有8个分支(由于if语句)。我将内部循环减少到每个字节 2 个分支。而switch语句是跳转表,所以是索引跳转。编译这个,优化速度。通过在循环中使用if 语句,这种设计将我的嵌入式系统速度提高了至少 30%。对其进行分析。
        • 我同意这可能值得一试。我也会从使用索引更改为使用指针增量 - 不仅因为我认为编译器可能会更容易优化它,而且我认为它更惯用和可读。哦,将byte &gt;&gt;= 1 更改为byte &gt;&gt;= 4
        【解决方案10】:

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

        【讨论】:

          【解决方案11】:

          立即想到什么:

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

          最好的办法是查看为您的目标平台生成的汇编器类型,并查看编译器在做什么。

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

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

          【讨论】:

          • 您能否详细说明缓存未命中假设?该表只有 8 个条目...
          • 256 个条目,每个可能的字节值一个。如果您将每个条目设为一个字节(您不需要更多),则您有 256 个字节。根据平台的不同,可能有 2-4 个缓存行被“阻塞”。
          • 实际上更糟,因为字节索引表中的每个条目都需要包含 8 个值(索引中的每个位一个 0x7f0x81)。所以它是 4096 字节还是 2048 字节,具体取决于您何时在表中存储 int16int8 值。
          【解决方案12】:

          首先,您这样做是为了实现 8 段显示,不是吗?

          你可能想要

          #include <stdint.h>
          

          它包含typedefs 用于大小整数,名称如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 &lt; 0。尝试在测试之前减少它通常会导致设置相同的标志,因为显式测试值是针对 0 的,而编译器通常会在优化开启时利用这一点。

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-01-28
            • 2022-11-26
            • 1970-01-01
            • 1970-01-01
            • 2016-02-02
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多