计数';1';C中的数字

计数';1';C中的数字,c,binary,C,Binary,我的任务是打印从2到N的所有整数(二进制中的“1”大于“0”) 最后,我搞砸了,不知道什么更好。你对此有何看法?甚至比你老师的建议更好: if( n & 1 ) { ++ CountOnes; } else { ++ CountZeros; } n%2有一个隐式除法操作,编译器可能会对其进行优化,但您不应该依赖它-除法是一个复杂的操作,在某些平台上需要更长的时间。此外,只有两个选项1或0,因此如果它不是一,则为零-无需在else块中进行第

我的任务是打印从2到N的所有整数(二进制中的“1”大于“0”)


最后,我搞砸了,不知道什么更好。你对此有何看法?

甚至比你老师的建议更好:

   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个操作(将
>=
计算为两个移位和赋值)。也许更重要的是,它更容易理解。

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

当试图找出执行某些操作的最佳方法时,您需要查看编译器生成的代码类型。看看CPU手册,看看在特定的体系结构上哪些操作是快的,哪些是慢的。虽然有一般性的指导方针。当然,如果有方法可以减少CPU必须执行的指令数量

我决定向您展示几种不同的方法(并非详尽无遗),并向您提供一个示例,说明如何着手手动优化小函数(如此函数)。有更复杂的工具可以帮助处理更大、更复杂的功能,但是这种方法应该适用于几乎任何东西:

注 所有汇编代码都是使用以下方法生成的:

gcc-O99-foo-fprofile生成foo.c

gcc-O99-o foo-fprofile使用foo.c

On-fprofile生成 双编译使gcc真正让gcc工作起来(尽管-O99很可能已经这样做了),但是根据您可能使用的gcc版本,差异可能会有所不同

关于它: 方法一(你) 以下是对函数的分解:

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

方法三 以下是克雷尼根的方法:

 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方法非常优化,循环中的操作数最少)。将克里尼根与天真的计数方法进行比较是很有指导意义的,但表面上看起来可能是一样的,实际上并非如此

for (c = 0; v; v >>= 1)
{
  c += v & 1;
}
这种方法比克里尼汉更糟糕。这里,如果你说第32位设置,这个循环将运行32次,而Krenighan的不会

但是,所有这些方法仍然相当低,因为它们是循环的

如果我们将一些其他的(隐式的)知识结合到我们的算法中,我们就可以一起摆脱循环。它们是,1,以位表示的数字大小,以位表示的字符大小。有了这些片段,并且意识到如果我们有一个64位寄存器,我们可以过滤掉14、24或32位块中的位

例如,如果我们看一个14位的数字,那么我们可以简单地通过以下方式计算位:

 (n * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;
0x0
0x3fff
之间的所有数字只使用一次

对于24位,我们使用14位,然后对剩余的10位使用类似的内容:

  ((n & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f 
+ (((n & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) 
 % 0x1f;
但是我们可以通过实现上面数字中的模式来推广这个概念,并认识到幻数实际上只是恭维(仔细看十六进制数0x8000+0x400+0x200+0x1)

我们可以在这里概括然后缩小思路,为我们提供计算位(最多128位)(无循环)O(1)的最优化方法:

CountOnes\u最佳(无符号整数n){
const unsigned char_bits=sizeof(unsigned char)>1)&(T)~(T)0/3);//将n重新用作临时变量
n=(n&(T)~(T)0/15*3)+(n>>2)&(T)~(T)0/15*3);
n=(n+(n>>4))&(T)~(T)0/255*15;
返回(T)(n*((T)~(T)0/255))>>(sizeof(T)-1)*字符位;
} 
最佳伯爵:
.LFB23:
.cfi_startproc
移动%edi,%eax
shrl%eax
andl$1431655765,%eax
子目录%eax,%edi
移动%edi,%edx
shrl$2,%edi
andl$858993459,%edx
andl$858993459,%edi
添加%edx,%edi
移动%edi,%ecx
shrl$4,%ecx
添加%edi,%ecx
和252645135美元,ecx%
I全额$16843009,%ecx,%eax
shrl$24,%eax
ret
.cfi_endproc
这可能是一个跳跃(你是怎么从以前到现在的),但只是花你的时间去看看它

AMD Athelon的软件优化指南中首次提到了最优化的方法™ 64和Opteron™ 处理器,我的那个URL坏了。这也是很好的解释上非常优秀
我强烈建议您仔细阅读该页面的内容,这真是一本很棒的书。

1不总是大于0吗?困惑,一怎么算零?你的意思是最重要的一个后面的零?@alk我猜这是关于popcnt大于或大于值宽度的一半。否则(n%2==
 (n * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;
  ((n & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f 
+ (((n & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) 
 % 0x1f;
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