我的任务是打印从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;
}
最后,我搞砸了,不知道什么是更好的。你怎么看待这个?
答案 0 :(得分:4)
我在下面的实验中使用了gcc编译器。您的编译器可能不同,因此您可能需要做一些不同的事情以获得类似的效果。
当试图找出最优化的方法来做某事时,你想看看编译器产生什么样的代码。查看CPU的手册,看看哪些操作很快,哪些操作在特定架构上很慢。虽然有一般指导方针。当然,如果有办法可以减少CPU必须执行的指令数量。
我决定向您展示一些不同的方法(并非详尽无遗),并为您提供一个如何手动查看小功能优化(如此)的示例。有更复杂的工具可以帮助实现更大,更复杂的功能,但是这种方法几乎可以用于任何事情:
所有汇编代码均使用:
生成gcc -O99 -o foo -fprofile-generate foo.c
接着是
gcc -O99 -o foo -fprofile-use foo.c
双重编译使gcc真的让gcc工作(虽然-O99很可能已经这样做了)但是milage可能因你可能使用的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条指令,直到循环退出
这是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;
使用%
,但0x0
和0x3fff
之间的所有数字只使用一次
对于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并指定)。更重要的是,或许更容易理解。