簡體   English   中英

在C中用數字計算'1'

[英]Counting '1' in number in C

我的任務是打印從2到N的所有整數(其中二進制數'1'大於'0')

int CountOnes(unsigned int x)
{ 
    unsigned int iPassedNumber = x; // number to be modifed
    unsigned int iOriginalNumber = iPassedNumber;
    unsigned int iNumbOfOnes = 0;

    while (iPassedNumber > 0)
    {
        iPassedNumber = iPassedNumber >> 1 << 1;  //if LSB was '1', it turns to '0'

        if (iOriginalNumber - iPassedNumber == 1) //if diffrence == 1, then we increment numb of '1'
        {
            ++iNumbOfOnes;
        }

        iOriginalNumber = iPassedNumber >> 1; //do this to operate with the next bit
        iPassedNumber = iOriginalNumber; 
    }
    return (iNumbOfOnes);
}

這是我計算二進制數'1'的函數。 這是我在大學里的作業。 但是,我的老師說會更有效率

{ 
   if(n%2==1)
      ++CountOnes;
   else(n%2==0)
      ++CountZeros;
}

最后,我搞砸了,不知道什么更好。 你怎么看待這件事?

我在下面的實驗中使用了gcc編譯器。 您的編譯器可能不同,因此您可能需要做一些不同的事情以獲得類似的效果。

當試圖找出最優化的方法來做某事時,你想看看編譯器產生什么樣的代碼。 查看CPU的手冊,看看哪些操作很快,哪些操作在特定架構上很慢。 雖然有一般指導方針。 當然,如果有辦法可以減少CPU必須執行的指令數量。

我決定向您展示一些不同的方法(並非詳盡無遺),並舉例說明如何手動查看小功能(如此)的優化。 有更復雜的工具可以幫助實現更大,更復雜的功能,但是這種方法幾乎適用於任何事情:

注意

所有匯編代碼均使用以下方式生

gcc -O99 -o foo -fprofile-generate foo.c

其次是

gcc -O99 -o foo -fprofile-use foo.c

在-fprofile-generate

雙重編譯使gcc真的讓gcc工作(雖然-O99很可能已經這樣做了)但是milage可能會根據你可能使用的gcc版本而有所不同。

隨它:

方法I(你)

這是你的功能的反匯編:

CountOnes_you:
.LFB20:
        .cfi_startproc
        xorl    %eax, %eax
        testl   %edi, %edi
        je      .L5
        .p2align 4,,10
        .p2align 3
.L4:
        movl    %edi, %edx
        xorl    %ecx, %ecx
        andl    $-2, %edx
        subl    %edx, %edi
        cmpl    $1, %edi
        movl    %edx, %edi
        sete    %cl
        addl    %ecx, %eax
        shrl    %edi
        jne     .L4
        rep ret
        .p2align 4,,10
        .p2align 3
.L5:
        rep ret
        .cfi_endproc

乍看上去

循環中大約有9條指令,直到循環退出

方法二(老師)

這是一個使用你老師的算法的功能:

int CountOnes_teacher(unsigned int x)
{
    unsigned int one_count = 0;
    while(x) {
        if(x%2)
            ++one_count;
        x >>= 1;
    }
    return one_count;
}

這是對它的反匯編:

CountOnes_teacher:
.LFB21:
        .cfi_startproc
        xorl    %eax, %eax
        testl   %edi, %edi
        je      .L12
        .p2align 4,,10
        .p2align 3
.L11:
        movl    %edi, %edx
        andl    $1, %edx
        cmpl    $1, %edx
        sbbl    $-1, %eax
        shrl    %edi
        jne     .L11
        rep ret
        .p2align 4,,10
        .p2align 3
.L12:
        rep ret
        .cfi_endproc

乍看上去:

循環中的5條指令,直到循環退出

方法III

這是Krenighan的方法:

 int CountOnes_K(unsigned int x) {
      unsigned int count;
      for(count = 0; ; x; count++) {
          x &= x - 1; // clear least sig bit
      }
      return count;
 }

這是反匯編:

CountOnes_k:
.LFB22:
        .cfi_startproc
        xorl    %eax, %eax
        testl   %edi, %edi
        je      .L19
        .p2align 4,,10
        .p2align 3
.L18: 
        leal    -1(%rdi), %edx
        addl    $1, %eax
        andl    %edx, %edi
        jne     .L18  ; loop is here
        rep ret
        .p2align 4,,10
        .p2align 3
.L19:
        rep ret
        .cfi_endproc

乍看上去

循環中的3條指令。

繼續之前的一些評論

正如您所看到的,當您使用%來計算(由您和您的老師使用)時,編譯器並沒有真正使用最佳方法。

Krenighan方法非常優化,循環中的操作次數最少)。 將Krenighan與天真的計數方法進行比較是有教育意義的,而從表面上看它可能看起來一樣,它實際上並非如此!

for (c = 0; v; v >>= 1)
{
  c += v & 1;
}

與Krenighans相比,這種方法很糟糕。 在這里,如果你說第32位設置這個循環將運行32次,而Krenighan不會!

但是所有這些方法仍然相當低,因為它們循環。

如果我們將其他一些(隱含的)知識結合到我們的算法中,我們可以將所有循環一起消除。 它們是1,我們的位數的大小,以及位的字符大小。 通過這些部分並實現我們可以過濾掉14位,24位或32位的塊,因為我們有64位寄存器。

因此,例如,如果我們查看一個14位數字,那么我們可以簡單地計算位數:

 (n * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;

0x00x3fff之間的所有數字使用%但只使用一次

對於24位,我們使用14位,然后對剩余的10位使用類似的位:

  ((n & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f 
+ (((n & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) 
 % 0x1f;

但是我們可以通過實現上面數字中的模式來概括這個概念,並且意識到幻數實際上只是恭維(看十六進制數密切接近0x8000 + 0x400 + 0x200 + 0x1)

我們可以概括然后縮小這里的想法,為我們提供最優化的計數位(最多128位)(無循環)O(1)的方法:

CountOnes_best(unsigned int n) {
    const unsigned char_bits = sizeof(unsigned char) << 3;
    typedef __typeof__(n) T; // T is unsigned int in this case;
    n = n - ((n >> 1) & (T)~(T)0/3); // reuse n as a temporary 
    n = (n & (T)~(T)0/15*3) + ((n >> 2) & (T)~(T)0/15*3);
    n = (n + (n >> 4)) & (T)~(T)0/255*15;
    return (T)(n * ((T)~(T)0/255)) >> (sizeof(T) - 1) * char_bits;
} 


CountOnes_best:
.LFB23:
        .cfi_startproc
        movl    %edi, %eax
        shrl    %eax
        andl    $1431655765, %eax
        subl    %eax, %edi
        movl    %edi, %edx
        shrl    $2, %edi
        andl    $858993459, %edx
        andl    $858993459, %edi
        addl    %edx, %edi
        movl    %edi, %ecx
        shrl    $4, %ecx
        addl    %edi, %ecx
        andl    $252645135, %ecx
        imull   $16843009, %ecx, %eax
        shrl    $24, %eax
        ret
        .cfi_endproc

這可能是一個跳躍(你從前一次到這里怎么樣),但只是花時間去討論它。

最優化的方法首先在AMD Athelon™64和Opteron™處理器的軟件優化指南中提到,我的URL已被破壞。 在非常優秀的C bit twiddling頁面上也很好地解釋了我強烈建議瀏覽該頁面的內容它真的是一個很棒的閱讀。

老師的建議更好:

   if( n & 1 ) {
      ++ CountOnes;
   }
   else {
      ++ CountZeros;
   }

n % 2有一個隱式除法運算,編譯器可能會優化它,但你不應該依賴它 - 除法是一個復雜的操作,在某些平台上需要更長的時間。 此外,只有兩個選項1或0,所以如果它不是一個,則為零 - 不需要在else塊中進行第二次測試。

您的原始代碼過於復雜且難以理解。 如果要評估算法的“效率”,請考慮每次迭代執行的操作數和迭代次數。 還涉及變量的數量。 在你的情況下,每次迭代有10個操作和三個變量(但是你省略了計算零,所以你需要四個變量才能完成賦值)。 下列:

unsigned int n = x; // number to be modifed
int ones = 0 ;
int zeroes = 0 ;

while( i > 0 )
{
   if( (n & 1) != 0 )
   {
      ++ones ;
   }
   else
   {
      ++zeroes ;
   }

   n >>= 1 ;
}

只有7個操作(計數>>=作為兩個移位和分配 )。 或許更重要的是,它更容易遵循。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM