x / 2和x的差异>> 1或x * 2且x <&lt;&lt; 1其中x是整数

时间:2014-02-03 16:59:46

标签: c++ bit-manipulation multiplication integer-division

正如我们所知的计算整数x / 2,我们只是为x * 2编写y=x/2;;但优秀的程序员使用位操作来计算它。

他们只是做y = x >> 1;

这两种方法有什么区别吗? 差异,我的意思是所需的时间/空间/内存的差异或两者完全相同(即x> 2由x&gt;&gt; 1实现)?

同样是乘法/除法与其他数字而不是2以相同的方式实现(即5*5 = 10*2 + 5*1 = 10 << 1 + 5 = 25)?

10 个答案:

答案 0 :(得分:9)

这个问题已在ridiculousfishblog上得到解答:http://ridiculousfish.com/blog/posts/will-it-optimize.html

  
      
  1. 分2到右移
  2.         

    GCC会将整数除以2变换为右移吗?

int halve_it(int x) {
   return x / 2;
}

int halve_it(int x) {
   return x >> 1;
}
  

右移操作符等同于向前舍入的除法   负无穷大,但正常的分裂向零。就这样   建议的优化会产生奇数负的错误结果   号。

     

可以通过将最高有效位添加到“修正”结果   移位之前的分子,而gcc就是这样做的。

优秀的程序员让编译器优化他们的代码,除非他们遇到性能损失。

编辑:由于您要求提供官方来源,我们引用C99的标准理由文档。你可以在这里找到它:http://www.open-std.org/jtc1/sc22/wg14/www/docs/C99RationaleV5.10.pdf

  

在C89中,涉及负操作数的整数划分可以以实现定义的方式向上或向下舍入;目的是避免在运行时代码中产生开销以检查特殊情况并强制执行特定行为。但是,在Fortran中,结果将始终截断为零,并且数字编程社区似乎可以接受开销。因此,C99现在需要类似的行为,这应该有助于将代码从Fortran移植到C.本文档的第7.20.6.2节中的表说明了所需的语义。

你的优化在C89中是正确的,因为它让编译器按照自己的意愿去做。但是,C99引入了一个符合Fortran代码的新约定。以下是除法运算符的预期示例(始终来自同一文档):

enter image description here

不幸的是,您的优化不符合C99标准,因为它没有为x = -1提供正确的结果:

#include <stdio.h>

int div8(int x)
{
    return x/3;
}

int rs8( int x )
{
    return x >> 3;
}

int main(int argc, char *argv[])
{
    volatile int x = -1;
    printf("div : %d \n", div8(x) );
    printf("rs : %d \n", rs8(x) );

    return 0;
}

Result:
div : 0 
rs : -1 
[Finished in 0.2s]

如果查看已编译的代码,可以发现一个有趣的区别(使用g ++ v4.6.2编译):

0040138c <__Z4div8i>:
  40138c:   55                      push   %ebp
  40138d:   89 e5                   mov    %esp,%ebp
  40138f:   8b 45 08                mov    0x8(%ebp),%eax
  401392:   85 c0                   test   %eax,%eax
  401394:   79 03                   jns    401399 <__Z4div8i+0xd>
  401396:   83 c0 0f                add    $0x7,%eax
  401399:   c1 f8 04                sar    $0x3,%eax
  40139c:   5d                      pop    %ebp
  40139d:   c3                      ret    

0040139e <__Z3rs8i>:
  40139e:   55                      push   %ebp
  40139f:   89 e5                   mov    %esp,%ebp
  4013a1:   8b 45 08                mov    0x8(%ebp),%eax
  4013a4:   c1 f8 03                sar    $0x3,%eax
  4013a7:   5d                      pop    %ebp
  4013a8:   c3                      ret  

401392,有一条test指令,用于检查奇偶校验位,如果数字为负,则在右移3个单位之前将1 << (n-1) = 7加到x

答案 1 :(得分:8)

您应该对的含义进行编码,并在需要时进行优化。

据我所知,不同之处在于签名号码,其行为未定义。这可能是历史性的,因为存在其他信号机制而不是2的赞美,但实际上这意味着编译器可以使用在优化时可能无法按预期运行的指令。

答案 2 :(得分:3)

取决于。

通常,位操作比算术更快,尤其是乘法和除法。然而,许多(如果不是最优化的)编译器将为速度做正确的事情,因此无论写入哪个都无关紧要。

1978年用于CDC Cyber​​的Pascal编译器生成的代码用于移位和添加乘法,其中包含1或2位的常量。例如:

 x := somevar * 10;    /* 10 has two bits set */

会生成相当于

的代码
 x := (somevar << 1) + (somevar << 3);   /*  *2 + *8 */

这比使用整数乘法指​​令要快得多。

答案 3 :(得分:1)

按照惯例,优秀的程序员不会转移而不是乘法和分裂:即使它做同样的事情,它也不会更快,更神秘。

此外,它并不总是做同样的事情。

右移是算术还是逻辑取决于您的CPU架构:C允许。因此,-23 >> 1可以给出积极的结果。

答案 4 :(得分:1)

对于负数,

x/2不等于x >> 1。无论如何,如果可能的话,几乎每个编译器都会自动用2到位操作的能力代替乘法或除法。

答案 5 :(得分:1)

这个问题的整个前提都有一种过早的微观优化。

编写代码时,请将其编写为清晰。如果乘以数字,则将操作显示为乘法。

当/如果遇到性能障碍并且确定(通过分析)您的代码需要调整为“丑陋”版本(例如使用位移而不是乘法和除法)时,会出现异常。除非遇到这样的性能问题(不太可能),否则当你打算使用乘法(或除法)时,没有理由使用位移。

答案 6 :(得分:1)

表达式的评估取决于处理器体系结构,平台体系结构和编译器。

理论上,对于所有无符号整数,x >> 1x / 2相同 如果编译器足够智能或者正确设置了优化,则编译器应为每个编译器生成相同的可执行代码,前提是您的进程具有移位操作。

此外,x << 1x * 2对于所有无符号整数应该相同。

某些编译器可能无法识别相同的内容并实际执行x * 2的乘法和x / 2的除法。

事实将是您的编译器生成的汇编语言。

更大的问题是可读性。大多数人都熟悉乘法,因为它在学校的早期教学。二元移位对大多数人来说并不常见。我仍然受到程序员的关于轮班操作的质疑。如有疑问,请选择可读性而非性能。

答案 7 :(得分:0)

CPU得到一个逻辑电路来乘以数字,在intel x86中它是MUL指令。 优秀的程序员会像这样编写代码,并将整个事情包装得很好。

但是你肯定错过了通过假设x <1 = x * 2来检查溢出的逻辑,并且它只适用于无符号整数。您不能将负数除以x> 1或x <1,因为最右边的位是+/-的位。

答案 8 :(得分:0)

让我演示一个例子:

int f(int n){
  if(n<=0) return 33;
  if((n*2)<=0) return 42;
  return 0;
}
int g(int n){
  if(n<=0) return 33;
  if((n<<1)<=0) return 42;
  return 0;
}

您可能认为两个函数都做同样的事情。但是,让我们看一下f:

生成的asm
testl   %edi, %edi
movl    $0, %edx
movl    $33, %eax
cmovg   %edx, %eax
ret

和g:

testl   %edi, %edi
movl    $33, %eax
jle .L5
addl    %edi, %edi
movl    $0, %edx
movb    $42, %al
testl   %edi, %edi
cmovg   %edx, %eax
.L5:
rep ret

正如您所注意到的,编译器能够删除f的第二个比较,但不能删除g。原因是有符号运算可能不会溢出。试图聪明地使你的代码变慢。顺便说一句,请注意编译器发现用n + n替换n&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&gt;

答案 9 :(得分:0)

对于x86架构,考虑像gcc或VS这样的编译器,区别并不明显;实际上一些程序员会考虑使用&gt;&gt;或&lt;&lt;而不是/或*一种混淆形式。

嵌入式系统的差异很明显,您有不同的架构和编译器。这一切都归结为特定架构上可用的指令以及编译器的智能程度。

让我详细说明: 对于任何操作a op b,您需要考虑哪些事项?你应该考虑操作数和结果的数据类型。为什么这很重要?因为整数与十进制数的表示方式不同,当然也存在溢出问题(通常为16位* 16位= 32位)。 让我们采用乘法:在某些体系结构上,您可以获得多个操作数的乘法指令,如:

  16 bit * 16 bit = 32 bit
  16 bit * 16 bit = 16 bit
  16 bit * 32 bit = 32 bit
  .... 

根据您编写代码的方式以及编译器的智能程度,生成的代码将包含一定数量的指令(数字越小,代码执行的速度越快)。

到目前为止,*,/和&gt;&gt;,&lt;&lt;。

都是如此。

硬件通常支持乘法,并且您有一条指令可以获得结果(除非您正在处理架构本身不支持的数据类型,并且必须模拟指令 - 但这更复杂)。 然而,划分更昂贵并且通常被模拟:在重复循环中连续减法。因此,有更多的指示来获得结果。

有些编译器足够智能并且分析代码,生成用于除法/乘法的移位操作,其立即值为2;但编写像a=2; c= b*a;这样的代码可以使最聪明的编译器处于困境。

另一方面,移位更直接:保证支持指令,通常结果与操作数相同(16bi> 1 = 16位)。

考虑到所有这些,当你使用移位操作而不是乘法/除法时,你通常会帮助编译生成更好,更快的代码