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

fd3cxomn  于 2023-03-11  发布在  Java
关注(0)|答案(6)|浏览(109)

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

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

这两种方法中哪一种效率更高?

ckocjqey

ckocjqey1#

位操作几乎肯定更快。
除法/取模是一个 * 通用 * 运算,它必须适用于您提供的任何除数,而不仅仅是2。它还必须检查下溢、范围错误和除以零,并维护余数,所有这些都需要时间。
位操作只执行位“与”操作,在本例中恰好对应于除以2,实际上可能只使用一个处理器操作来执行。

cgh8pdjw

cgh8pdjw2#

要么&表达式会更快,要么它们的速度相同,上次我试过了,当我用2的时候它们的速度是一样的(因为编译器可以优化它),但是如果2在变量中,%会变慢。作为奇数测试的表达式x % 2 == 1不适用于负x。因此,“这至少是优选&的一个原因。

4szc88ey

4szc88ey3#

在实践中几乎不会有明显的差别,特别是,很难想象这种指令会成为实际瓶颈的情况。
(Some吹毛求疵:“二进制”运算应称为按位运算,而“模”运算实际上是余数运算)
从更理论化的观点来看,我们可以假设二进制运算比余数运算更有效,其原因在其他答案中已经指出。
不过,再回到实际的观点: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;
    }
}

使用热点反汇编程序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指令。
因此,重复一下这个答案中的第一句话:实际上几乎不会有明显的差别,不仅因为单个指令的开销几乎不会成为瓶颈,而且因为你永远不知道实际上会执行哪些指令,在这种特殊情况下,你必须假设它们基本上是相等的。

brc7rcf0

brc7rcf04#

实际上,这两个表达式都没有测试被2整除的可能性(除非是负数),如果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;
    }
}
7lrncoxx

7lrncoxx5#

二进制运算更快。求模运算必须计算一次除法才能得到余数。

7cwmlq89

7cwmlq896#

更多二进制运算选择:
x〉〉1〈〈1!=x
x〉〉1〈〈1^x
在python中测试:

for x in range(-5,6):
    print(x, x%2, (x&1)==1, x>>1<<1^x, x>>1<<1!=x)

相关问题