是否有更有效的方法来获取32位整数的长度(以字节为单位)?

时间:2010-08-30 16:02:50

标签: c++ c performance bit-manipulation bitwise-operators

我想要一个以下小功能的快捷方式,在哪里 性能非常重要(该函数调用超过10.000.000次):

inline int len(uint32 val)
{
    if(val <= 0x000000ff) return 1;
    if(val <= 0x0000ffff) return 2;
    if(val <= 0x00ffffff) return 3;
    return 4;
} 

有没有人有任何想法......一个很酷的bitoperation技巧? 感谢您的帮助!

14 个答案:

答案 0 :(得分:37)

这个怎么样?

inline int len(uint32 val)
{
    return 4
        - ((val & 0xff000000) == 0)
        - ((val & 0xffff0000) == 0)
        - ((val & 0xffffff00) == 0)
    ;
}

删除inline关键字,g++ -O2将此代码编译为以下无分支代码:

movl    8(%ebp), %edx
movl    %edx, %eax
andl    $-16777216, %eax
cmpl    $1, %eax
sbbl    %eax, %eax
addl    $4, %eax
xorl    %ecx, %ecx
testl   $-65536, %edx
sete    %cl
subl    %ecx, %eax
andl    $-256, %edx
sete    %dl
movzbl  %dl, %edx
subl    %edx, %eax

如果您不介意特定于机器的解决方案,可以使用bsr指令搜索前1位。然后你只需将8除以将位转换为字节并加1以将范围0..3移位到1..4:

int len(uint32 val)
{
    asm("mov 8(%ebp), %eax");
    asm("or  $255, %eax");
    asm("bsr %eax, %eax");
    asm("shr $3, %eax");
    asm("inc %eax");
    asm("mov %eax, 8(%ebp)");
    return val;
}

请注意,我不是内联汇编之神,所以也许有更好的解决方案来访问val而不是显式地寻址堆栈。但是你应该得到基本的想法。

GNU编译器还有一个有趣的内置函数__builtin_clz

inline int len(uint32 val)
{
    return ((__builtin_clz(val | 255) ^ 31) >> 3) + 1;
}

这看起来比内联汇编版本要好得多:)

答案 1 :(得分:24)

在VS 2010编译器下,在0到MAX_LONG次循环中调用函数时,我做了一个小的不科学的基准测试,只测量GetTickCount()调用的差异。

这就是我所看到的:

这需要11497个刻度

inline int len(uint32 val)
{
    if(val <= 0x000000ff) return 1;
    if(val <= 0x0000ffff) return 2;
    if(val <= 0x00ffffff) return 3;
    return 4;
} 

虽然这需要14399个刻度

inline int len(uint32 val)
{
    return 4
        - ((val & 0xff000000) == 0)
        - ((val & 0xffff0000) == 0)
        - ((val & 0xffffff00) == 0)
    ;
}

编辑:我对于为什么一个更快的想法是错误的,因为:

inline int len(uint32 val)
{
    return 1
        + (val > 0x000000ff)
        + (val > 0x0000ffff)
        + (val > 0x00ffffff)
        ;
}

此版本仅使用了11107个刻度。因为+快于 - 也许?我不确定。

更快但是在7161滴答的二进制搜索

inline int len(uint32 val)
{
    if (val & 0xffff0000) return (val & 0xff000000)? 4: 3;
    return (val & 0x0000ff00)? 2: 1;
}

到目前为止最快的是使用MS内在函数,在4399个刻度

#pragma intrinsic(_BitScanReverse)

inline int len2(uint32 val)
{
    DWORD index;
    _BitScanReverse(&index, val);

    return (index>>3)+1;

}

供参考 - 这是我用来描述的代码:

int _tmain(int argc, _TCHAR* argv[])
{
    int j = 0;
    DWORD t1,t2;

    t1 = GetTickCount();

    for(ULONG i=0; i<-1; i++)
        j=len(i);

    t2 = GetTickCount();

    _tprintf(_T("%ld ticks %ld\n"), t2-t1, j);


    t1 = GetTickCount();

    for(ULONG i=0; i<-1; i++)
        j=len2(i);

    t2 = GetTickCount();

    _tprintf(_T("%ld ticks %ld\n"), t2-t1, j);
}

必须打印j以防止循环被优化。

答案 2 :(得分:14)

您是否真的有个人资料证明这是您申请中的重大瓶颈?只是以最明显的方式做到这一点,并且只有当分析显示它是一个问题(我怀疑)时,然后尝试改进。很可能通过减少对此函数的调用次数而不是通过更改其中的内容来获得最佳改进。

答案 3 :(得分:10)

二进制搜索可以节省几个周期,具体取决于处理器架构。

inline int len(uint32 val)
{
    if (val & 0xffff0000) return (val & 0xff000000)? 4: 3;
    return (val & 0x0000ff00)? 2: 1;
}

或者,找出哪个是最常见的情况可能会降低平均周期数,如果大多数输入是一个字节(例如,当构建UTF-8编码时,但是那时你的断点不会是32/24 / 16/8):

inline int len(uint32 val)
{
    if (val & 0xffffff00) {
       if (val & 0xffff0000) {
           if (val & 0xff000000) return 4;
           return 3;
       }
       return 2;
    }
    return 1;
}

现在,短案是最少的条件测试。

答案 4 :(得分:5)

如果位操作比目标机器上的比较快,则可以执行以下操作:

inline int len(uint32 val)
{
    if(val & 0xff000000) return 4;
    if(val & 0x00ff0000) return 3;
    if(val & 0x0000ff00) return 2;
    return 1;
} 

答案 5 :(得分:3)

如果数字的分布不能使预测变得容易,则可以避免条件分支成本高昂:

return 4 - (val <= 0x000000ff) - (val <= 0x0000ffff) - (val <= 0x00ffffff);

<=更改为&在现代处理器上不会发生任何变化。你的目标平台是什么?

以下是带有gcc -O x86-64的生成代码:

    cmpl    $255, %edi
    setg    %al
    movzbl  %al, %eax
    addl    $3, %eax
    cmpl    $65535, %edi
    setle   %dl
    movzbl  %dl, %edx
    subl    %edx, %eax
    cmpl    $16777215, %edi
    setle   %dl
    movzbl  %dl, %edx
    subl    %edx, %eax

当然有比较说明cmpl,但后面跟着setgsetle而不是条件分支(通常情况下)。这是条件分支,在现代流水线处理器上很昂贵,而不是比较。所以这个版本保存了昂贵的条件分支。

我尝试手动优化gcc的程序集:

    cmpl    $255, %edi
    setg    %al
    addb    $3, %al
    cmpl    $65535, %edi
    setle   %dl
    subb    %dl, %al
    cmpl    $16777215, %edi
    setle   %dl
    subb    %dl, %al
    movzbl  %al, %eax

答案 6 :(得分:3)

在某些系统上,这可能会在某些架构上更快:

inline int len(uint32_t val) {
   return (int)( log(val) / log(256) );  // this is the log base 256 of val
}

这也可能稍快一些(如果比较需要比按位长的话):

inline int len(uint32_t val) {
    if (val & ~0x00FFffFF) {
        return 4;
    if (val & ~0x0000ffFF) {
        return 3;
    }
    if (val & ~0x000000FF) {
        return 2;
    }
    return 1;

}

如果您使用的是8位微控制器(如8051或AVR),那么这将最有效:

inline int len(uint32_t val) {
    union int_char { 
          uint32_t u;
          uint8_t a[4];
    } x;
    x.u = val; // doing it this way rather than taking the address of val often prevents
               // the compiler from doing dumb things.
    if (x.a[0]) {
        return 4;
    } else if (x.a[1]) {
       return 3;
    ...

由tristopia编辑:最后一个变体的字节序感知版本

int len(uint32_t val)
{
  union int_char {
        uint32_t u;
        uint8_t a[4];
  } x;
  const uint16_t w = 1;

  x.u = val;
  if( ((uint8_t *)&w)[1]) {   // BIG ENDIAN (Sparc, m68k, ARM, Power)
     if(x.a[0]) return 4;
     if(x.a[1]) return 3;
     if(x.a[2]) return 2;
  }
  else {                      // LITTLE ENDIAN (x86, 8051, ARM)
    if(x.a[3]) return 4;
    if(x.a[2]) return 3;
    if(x.a[1]) return 2;
  }
  return 1;
}

由于const,任何值得盐的编译器都只生成正确的字符串代码。

答案 7 :(得分:3)

根据您的架构,您可能拥有更高效的解决方案。

MIPS有一条“CLZ”指令,用于计算数字的前导零位数。你在这里寻找的基本上是4 - (CLZ(x) / 8)(其中/是整数除法)。 PowerPC具有等效指令cntlz,x86具有BSR。该解决方案应简化为3-4条指令(不计算函数调用开销)和零分支。

答案 8 :(得分:2)

只是为了说明,基于FredOverflow的答案(这是很好的工作,荣誉和+1),关于x86分支的常见缺陷。这是FredOverflow的汇编作为gcc的输出:

movl    8(%ebp), %edx   #1/.5
movl    %edx, %eax      #1/.5
andl    $-16777216, %eax#1/.5
cmpl    $1, %eax        #1/.5
sbbl    %eax, %eax      #8/6
addl    $4, %eax        #1/.5
xorl    %ecx, %ecx      #1/.5
testl   $-65536, %edx   #1/.5
sete    %cl             #5
subl    %ecx, %eax      #1/.5
andl    $-256, %edx     #1/.5
sete    %dl             #5
movzbl  %dl, %edx       #1/.5
subl    %edx, %eax      #1/.5
# sum total: 29/21.5 cycles

(延迟,以周期为单位,将被视为Prescott / Northwood)

Pascal Cuoq手工优化组装(也称赞):

cmpl    $255, %edi      #1/.5
setg    %al             #5
addb    $3, %al         #1/.5
cmpl    $65535, %edi    #1/.5
setle   %dl             #5
subb    %dl, %al        #1/.5
cmpl    $16777215, %edi #1/.5
setle   %dl             #5
subb    %dl, %al        #1/.5
movzbl  %al, %eax       #1/.5
# sum total: 22/18.5 cycles

使用__builtin_clz()编辑:FredOverflow的解决方案:

movl 8(%ebp), %eax   #1/.5
popl %ebp            #1.5
orb  $-1, %al        #1/.5
bsrl %eax, %eax      #16/8
sarl $3, %eax        #1/4
addl $1, %eax        #1/.5
ret
# sum total: 20/13.5 cycles

和代码的gcc程序集:

movl $1, %eax        #1/.5
movl %esp, %ebp      #1/.5
movl 8(%ebp), %edx   #1/.5
cmpl $255, %edx      #1/.5
jbe  .L3             #up to 9 cycles
cmpl $65535, %edx    #1/.5
movb $2, %al         #1/.5
jbe  .L3             #up to 9 cycles
cmpl $16777216, %edx #1/.5
sbbl %eax, %eax      #8/6
addl $4, %eax        #1/.5
.L3:
ret
# sum total: 16/10 cycles - 34/28 cycles

其中指令高速缓存行提取作为jcc指令的副作用可能对这样的短函数没有任何成本。

分支是一个合理的选择,具体取决于输入分布。

编辑:添加了使用__builtin_clz()的FredOverflow解决方案。

答案 9 :(得分:1)

还有一个版本。与Fred的相似,但操作较少。

inline int len(uint32 val)
{
    return 1
        + (val > 0x000000ff)
        + (val > 0x0000ffff)
        + (val > 0x00ffffff)
    ;
}

答案 10 :(得分:1)

这样可以减少比较。但如果内存访问操作的成本高于几个比较,则可能效率较低。

int precalc[1<<16];
int precalchigh[1<<16];
void doprecalc()
{
    for(int i = 0; i < 1<<16; i++) {
        precalc[i] = (i < (1<<8) ? 1 : 2);
        precalchigh[i] = precalc[i] + 2;
    }
}
inline int len(uint32 val)
{
    return (val & 0xffff0000 ? precalchigh[val >> 16] : precalc[val]);
}

答案 11 :(得分:1)

存储整数所需的的最小数量为:

int minbits = (int)ceil( log10(n) / log10(2) ) ;

bytes 的数量为:

int minbytes = (int)ceil( log10(n) / log10(2) / 8 ) ;

这完全是FPU绑定解决方案,性能可能会或可能不会比条件测试更好,但也许值得调查。

[编辑] 我做了调查;上面一千万次迭代的简单循环需要918ms,而FredOverflow接受的解决方案只用了49ms(VC ++ 2010)。因此,这不是性能方面的改进,但如果它是所需的位数,则可能仍然有用,并且可以进一步优化。

答案 12 :(得分:1)

帕斯卡尔库克和另外35位投票的人:

“哇!超过1000万次...你的意思是如果你从这个功能中挤出三个循环,你将节省多达0.03秒?”

这样的讽刺评论充其量粗鲁和冒犯。

优化通常是3%的累积结果,其中2%。总体容量的3%是没什么要打喷嚏。假设这是管道中几乎饱和且不可平行的阶段。假设CPU利用率从99%上升到96%。简单排队理论告诉人们,CPU利用率的这种降低会使平均队列长度减少75%以上。 [定性(负荷除以1负荷)]

这种减少可能经常造成或破坏特定的硬件配置,因为这会对内存要求产生反馈效应,缓存排队的项目,锁定convoying,以及(如果它是分页系统的恐怖恐怖)甚至是分页。正是这些效应导致分叉磁滞回线型系统行为。

任何东西的到货率似乎都会上升,而特定CPU的现场更换或购买更快的盒子往往不是一种选择。

优化不仅仅是桌面上的挂钟时间。任何认为对计算机程序行为的测量和建模有很多阅读的人。

Pascal Cuoq欠原始海报道歉。

答案 13 :(得分:0)

如果我记得80x86 asm,我会做类似的事情:

  ; Assume value in EAX; count goes into ECX
  cmp eax,16777215 ; Carry set if less
  sbb ecx,ecx      ; Load -1 if less, 0 if greater
  cmp eax,65535
  sbb ecx,0        ; Subtract 1 if less; 0 if greater
  cmp eax,255
  sbb ecx,-4       ; Add 3 if less, 4 if greater

六条指示。我认为相同的方法也适用于我使用的ARM上的六条指令。