在C中用数字计算'1'

时间:2017-10-07 15:29:21

标签: c binary

我的任务是打印从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;
}

最后,我搞砸了,不知道什么是更好的。你怎么看待这个?

2 个答案:

答案 0 :(得分:4)

我在下面的实验中使用了gcc编译器。您的编译器可能不同,因此您可能需要做一些不同的事情以获得类似的效果。

当试图找出最优化的方法来做某事时,你想看看编译器产生什么样的代码。查看CPU的手册,看看哪些操作很快,哪些操作在特定架构上很慢。虽然有一般指导方针。当然,如果有办法可以减少CPU必须执行的指令数量。

我决定向您展示一些不同的方法(并非详尽无遗),并为您提供一个如何手动查看小功能优化(如此)的示例。有更复杂的工具可以帮助实现更大,更复杂的功能,但是这种方法几乎可以用于任何事情:

注意

所有汇编代码均使用:

生成
  

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

接着是

  

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

On -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条指令,直到循环退出

方法II(老师)

这是一个使用老师算法的功能:

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 page上也有很好的解释 我强烈建议查看该页面的内容,这真是一个很棒的阅读。

答案 1 :(得分:2)

更好的是老师的建议:

a

if( n & 1 ) { ++ CountOnes; } else { ++ CountZeros; } 有一个隐式除法运算,编译器可能会优化它,但你不应该依赖它 - divide是一个复杂的操作,在某些平台上需要更长的时间。此外,只有两个选项1或0,所以如果它不是一个,则为零 - 不需要在n % 2块中进行第二次测试。

您的原始代码过于复杂且难以理解。如果你想评估效率&#34;算法的一部分,考虑每次迭代执行的操作数和迭代次数。还涉及变量的数量。在你的情况下,每次迭代有10个操作和三个变量(但你省略了计算零,所以你需要四个变量才能完成赋值)。以下内容:

else

只有7个操作(将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 ; } 计为两个 - shift并指定)。更重要的是,或许更容易理解。