簡體   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