二进制运算比模数更有效吗?

时间:2014-09-10 21:47:36

标签: java

有两种方法可以检查数字是否可被2整除:

  • x % 2 == 1
  • (x & 1) == 1

两者中哪一个效率更高?

5 个答案:

答案 0 :(得分:5)

位操作几乎肯定更快。

除法/模数是广义操作,它必须适用于您提供的任何除数,而不仅仅是2.它还必须检查下溢,范围错误和除以零,并保持余数,所有这需要时间。

位操作只是做了一点“和”操作,在这种情况下恰好相应于除以2。它实际上可能只使用一个处理器操作来执行。

答案 1 :(得分:2)

&表达式会更快或速度相同。上次我试过,当我使用文字2时(因为编译器可以优化它),它们的速度是相同的,但如果%在变量中,则2会慢一些。 作为奇数测试的表达式x % 2 == 1不适用于否定x。 所以至少有一个理由更喜欢&

答案 2 :(得分:1)

实际上,这两个表达式都没有用两个方法来测试可分性(除了否定之外)。如果 x为奇数,它们实际上都会解析为true

还有许多其他测试偶数/奇数的方法(例如((x / 2) * 2) == x)),但它们都没有x & 1的最佳属性,因为没有编译器可能会错误并使用分割

大多数现代编译器会将x % 2编译为与x & 1相同的代码,但是一个特别愚蠢的可能使用除法运算来实现x % 2所以它可能效率低下。

关于更好的争论是另一回事。一个新手/累了的程序员可能不会认为x & 1是对奇数的测试,但是x % 2会更清楚,所以有一个论点x % 2会是更好的选择。

我 - 我去if ( Maths.isEven(x) )明确表达我的意思。恕我直言效率在列表中方式,远远超过清晰度可读性

public class Maths {

    // Method is final to encourage compiler to inline if it is bright enough.
    public static final boolean isEven(long n) {
        /* All even numbers have their lowest-order bit set to 0.
         * This `should` therefore be the most efficient way to recognise
         * even numbers.
         * Also works for negative numbers.
         */
        return (n & 1) == 0;
    }
}

答案 3 :(得分:1)

在实践中几乎没有明显的差异。特别是,很难想象这样的指令会成为实际的瓶颈。

(一些挑剔:“二进制”操作应该被称为按位操作,而“模数”操作实际上是余数操作)

从更理论的角度来看,人们可以假设二元运算比其余运算更有效,原因已在其他答案中指出过。

然而,再次回到实际的观点:JIT几乎肯定会来救援。考虑以下(非常简单)测试:

class BitwiseVersusMod
{
    public static void main(String args[])
    {
        for (int i=0; i<10; i++)
        {
            for (int n=100000; n<=100000000; n*=10)
            {
                long s0 = runTestBitwise(n);
                System.out.println("Bitwise sum "+s0);

                long s1 = runTestMod(n);
                System.out.println("Mod    sum "+s1);
            }
        }
    }


    private static long runTestMod(int n)
    {
        long sum = 0;
        for (int i=0; i<n; i++)
        {
            if (i % 2 == 1)
            {
                sum += i;
            }
        }
        return sum;
    }

    private static long runTestBitwise(int n)
    {
        long sum = 0;
        for (int i=0; i<n; i++)
        {
            if ((i & 1) == 1)
            {
                sum += i;
            }
        }
        return sum;
    }
}

使用

使用Hotspot反汇编程序VM运行它
java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly BitwiseVersusMod

创建JIT反汇编日志。

实际上,对于模数版本的第一次调用,它会创建以下反汇编:

  ...
  0x00000000027dcae6: cmp    $0xffffffff,%ecx
  0x00000000027dcae9: je     0x00000000027dcaf2
  0x00000000027dcaef: cltd   
  0x00000000027dcaf0: idiv   %ecx               ;*irem
                                                ; - BitwiseVersusMod::runTestMod@11 (line 26)
                                                ; implicit exception: dispatches to 0x00000000027dcc18
  0x00000000027dcaf2: cmp    $0x1,%edx
  0x00000000027dcaf5: movabs $0x54fa0888,%rax   ;   {metadata(method data for {method} {0x0000000054fa04b0} &apos;runTestMod&apos; &apos;(I)J&apos; in &apos;BitwiseVersusMod&apos;)}
  0x00000000027dcaff: movabs $0xb0,%rdx
  ....

其中irem指令被转换为idiv,这被认为是相当昂贵的。

与此相反,二进制版本使用and指令进行决策,如预期的那样:

  ....
  0x00000000027dc58c: nopl   0x0(%rax)
  0x00000000027dc590: mov    %rsi,%rax
  0x00000000027dc593: and    $0x1,%eax
  0x00000000027dc596: cmp    $0x1,%eax
  0x00000000027dc599: movabs $0x54fa0768,%rax   ;   {metadata(method data for {method} {0x0000000054fa0578} &apos;runTestBitwise&apos; &apos;(I)J&apos; in &apos;BitwiseVersusMod&apos;)}
  0x00000000027dc5a3: movabs $0xb0,%rbx
  ....

但是,对于最终的优化版本,生成的代码对于两个版本更相似。在这两种情况下,编译器都会进行大量的循环展开,但仍然可以识别方法的核心:对于按位版本,它会生成一个包含以下指令的展开循环:

  ...
  0x00000000027de2c7: mov    %r10,%rax
  0x00000000027de2ca: mov    %r9d,%r11d
  0x00000000027de2cd: add    $0x4,%r11d         ;*iinc
                                                ; - BitwiseVersusMod::runTestBitwise@21 (line 37)

  0x00000000027de2d1: mov    %r11d,%r8d
  0x00000000027de2d4: and    $0x1,%r8d
  0x00000000027de2d8: cmp    $0x1,%r8d
  0x00000000027de2dc: jne    0x00000000027de2e7  ;*if_icmpne
                                                ; - BitwiseVersusMod::runTestBitwise@13 (line 39)

  0x00000000027de2de: movslq %r11d,%r10
  0x00000000027de2e1: add    %rax,%r10          ;*ladd
                                                ; - BitwiseVersusMod::runTestBitwise@19 (line 41)
  ...

仍有and指令用于测试最低位。但对于模数版本,展开循环的核心是

  ...
  0x00000000027e3a0a: mov    %r11,%r10
  0x00000000027e3a0d: mov    %ebx,%r8d
  0x00000000027e3a10: add    $0x2,%r8d          ;*iinc
                                                ; - BitwiseVersusMod::runTestMod@21 (line 24)

  0x00000000027e3a14: test   %r8d,%r8d
  0x00000000027e3a17: jl     0x00000000027e3a2e  ;*irem
                                                ; - BitwiseVersusMod::runTestMod@11 (line 26)

  0x00000000027e3a19: mov    %r8d,%r11d
  0x00000000027e3a1c: and    $0x1,%r11d
  0x00000000027e3a20: cmp    $0x1,%r11d
  0x00000000027e3a24: jne    0x00000000027e3a2e  ;*if_icmpne
                                                ; - BitwiseVersusMod::runTestMod@13 (line 26)
  ...

我必须承认,我无法完全理解(至少在合理的时间内)它正在做什么完全。但在任何情况下:irem字节码指令也使用and汇编指令实现,并且在生成的机器代码中不再有任何 idiv指令

所以重复这个答案的第一个陈述:在实践中几乎没有明显的差别。不仅因为单个指令的成本几乎不会成为瓶颈,而且因为你永远不知道实际执行哪些指令,在这种特殊情况下,你必须假设它们基本上是相等的。

答案 4 :(得分:0)

二进制操作更快。 mod操作必须计算除法以获得余数。