繁体   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