繁体   English   中英

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

[英]Is there a more efficient way to get the length of a 32bit integer in bytes?

我想要一个以下小函数的快捷方式,其中性能非常重要(该函数被调用超过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技巧? 感谢您的帮助!

这个怎么样?

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

如果您不介意特定于机器的解决方案,可以使用搜索前1位的bsr指令。 然后,您只需将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;
}

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

我做了一个迷你的不科学的基准测试,只是在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以防止循环被优化。

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

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

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

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

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

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

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

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

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

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编辑:最后一个变体的endianness感知版本

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,任何值得盐的编译器只会生成正确的字节序的代码。

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

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

只是为了说明,基于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解决方案。

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

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

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

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

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

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

字节数是:

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

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

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

Pascal Cuoq和其他35位投票评论的人:

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

这种讽刺的评论充其量是粗鲁无礼的。

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

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

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

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

Pascal Cuoq欠原始海报道歉。

如果我记得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上的六条指令。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM