【问题标题】:Is there a useful difference between (p ^ q) and (p != q) for booleans?对于布尔值,(p ^ q) 和 (p != q) 之间是否有有用的区别?
【发布时间】:2019-08-31 01:40:21
【问题描述】:

Java 有两种方法来检查两个布尔值是否不同。您可以将它们与!=^ (xor) 进行比较。当然,这两个运算符在所有情况下都会产生相同的结果。尽管如此,正如在What's the difference between XOR and NOT-EQUAL-TO? 中所讨论的那样,将它们都包含在内是有意义的。甚至对于开发人员来说,根据上下文更喜欢一个而不是另一个是有意义的——有时“这些布尔值中的一个是真的”读起来更好,而其他时候“这两个布尔值是否不同”可以更好地传达意图。所以,也许使用哪一个应该是品味和风格的问题。

令我惊讶的是 javac 并没有同等对待这些!考虑这个类:

class Test {
  public boolean xor(boolean p, boolean q) {
    return p ^ q;
  }
  public boolean inequal(boolean p, boolean q) {
    return p != q;
  }
}

显然,这两种方法具有相同的可见行为。但是它们有不同的字节码:

$ javap -c Test
Compiled from "Test.java"
class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public boolean xor(boolean, boolean);
    Code:
       0: iload_1
       1: iload_2
       2: ixor
       3: ireturn

  public boolean inequal(boolean, boolean);
    Code:
       0: iload_1
       1: iload_2
       2: if_icmpeq     9
       5: iconst_1
       6: goto          10
       9: iconst_0
      10: ireturn
}

如果我不得不猜测,我会说xor 表现更好,因为它只是返回比较的结果;添加跳跃和额外的负载似乎是浪费工作。但我没有猜测,而是使用 Clojure 的“标准”基准测试工具对这两种方法的数十亿次调用进行了基准测试。它足够接近,虽然 xor 看起来有点快,但我在统计方面还不够好,无法说出结果是否显着:

user=> (let [t (Test.)] (bench (.xor t true false)))
Evaluation count : 4681301040 in 60 samples of 78021684 calls.
             Execution time mean : 4.273428 ns
    Execution time std-deviation : 0.168423 ns
   Execution time lower quantile : 4.044192 ns ( 2.5%)
   Execution time upper quantile : 4.649796 ns (97.5%)
                   Overhead used : 8.723577 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 25.4745 % Variance is moderately inflated by outliers
user=> (let [t (Test.)] (bench (.inequal t true false)))
Evaluation count : 4570766220 in 60 samples of 76179437 calls.
             Execution time mean : 4.492847 ns
    Execution time std-deviation : 0.162946 ns
   Execution time lower quantile : 4.282077 ns ( 2.5%)
   Execution time upper quantile : 4.813433 ns (97.5%)
                   Overhead used : 8.723577 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 22.2554 % Variance is moderately inflated by outliers

有什么理由更喜欢写一个而不是另一个,性能方面1?在某些情况下,它们的实施差异使一种比另一种更合适?或者,有谁知道为什么 javac 实现这两个相同的操作如此不同?

1 当然,我不会贸然利用这些信息进行微优化。我只是好奇这一切是如何运作的。

【问题讨论】:

  • 引入测试和分支显然会对性能产生一些影响。多少取决于多种因素,其中最重要的是该分支的可预测性。关于这个问题的大量现有技术;我会无耻地插入my own answer 作为起点。我无法发布实际答案,因为我不熟悉 Java 字节码是如何被翻译成机器码的。中间是否有优化器?可能是。无论哪种方式,都要提防过早的微优化。先写代码说出你的意思。
  • p != q 建议使用比较指令,而p ^ q 建议使用xor 指令。这就是你在字节码中看到的。如果以这种自然的方式进一步编译为机器码,那么p ^ q 如果将结果用作数字或存储到内存中可能会快一些,但如果用作分支条件会稍微慢一些。
  • 为什么p ^ q 会“如果用作分支条件会稍微慢一些”,@zch?
  • @CodyGray 实际上,从字节码进行的翻译很复杂,并且涉及到优化器。通常,字节码会被解释一段时间,并且只有在确定为运行时性能热点时才会将其 JIT 编译为本机代码。 JIT 优化器可以使用运行时信息来指导它的优化——我不是专家,但我想它可以使用它来指导它的分支预测,例如。这是 JVM 基准测试像标准一样“预热 JIT”很重要的原因之一。
  • @CodyGray,但如果编译器使用xor 并且它直接作为标志,在某些情况下它仍然会破坏优化,因为它会改变保存p(或q)的寄存器。跨度>

标签: java performance


【解决方案1】:

好吧,我将很快提供 CPU 如何翻译并更新帖子,但与此同时,您看到的差异太小了,无法在意。

java 中的字节码并不表示方法执行的速度(或不执行速度),有两个 JIT 编译器一旦足够热,就会使该方法看起来完全不同。众所周知,javac编译代码后只做很少的优化,真正的优化来自JIT

我已经为此使用JMH 进行了一些测试,仅使用C1 编译器或将C2 替换为GraalVM 或根本没有JIT...(大量测试代码如下,你可以跳过它,只看结果,这是使用jdk-12 btw 完成的)。这段代码使用了JMH——在 Java 世界的微基准测试中使用的事实上的工具(众所周知,如果手动完成的话,很容易出错)。

@Warmup(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
public class BooleanCompare {

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(BooleanCompare.class.getName())
            .build();

        new Runner(opt).run();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(1)
    public boolean xor(BooleanExecutionPlan plan) {
        return plan.booleans()[0] ^ plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(1)
    public boolean plain(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-Xint")
    public boolean xorNoJIT(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-Xint")
    public boolean plainNoJIT(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
    public boolean xorC2Only(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:-TieredCompilation")
    public boolean plainC2Only(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
    public boolean xorC1Only(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1, jvmArgsAppend = "-XX:TieredStopAtLevel=1")
    public boolean plainC1Only(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1,
        jvmArgsAppend = {
            "-XX:+UnlockExperimentalVMOptions",
            "-XX:+EagerJVMCI",
            "-Dgraal.ShowConfiguration=info",
            "-XX:+UseJVMCICompiler",
            "-XX:+EnableJVMCI"
        })
    public boolean xorGraalVM(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Fork(value = 1,
        jvmArgsAppend = {
            "-XX:+UnlockExperimentalVMOptions",
            "-XX:+EagerJVMCI",
            "-Dgraal.ShowConfiguration=info",
            "-XX:+UseJVMCICompiler",
            "-XX:+EnableJVMCI"
        })
    public boolean plainGraalVM(BooleanExecutionPlan plan) {
        return plan.booleans()[0] != plan.booleans()[1];
    }

}

结果:

BooleanCompare.plain         avgt    2    3.125          ns/op
BooleanCompare.xor           avgt    2    2.976          ns/op

BooleanCompare.plainC1Only   avgt    2    3.400          ns/op
BooleanCompare.xorC1Only     avgt    2    3.379          ns/op

BooleanCompare.plainC2Only   avgt    2    2.583          ns/op
BooleanCompare.xorC2Only     avgt    2    2.685          ns/op

BooleanCompare.plainGraalVM  avgt    2    2.980          ns/op
BooleanCompare.xorGraalVM    avgt    2    3.868          ns/op

BooleanCompare.plainNoJIT    avgt    2  243.348          ns/op
BooleanCompare.xorNoJIT      avgt    2  201.342          ns/op

我不是一个多才多艺的人来阅读汇编程序,虽然我有时喜欢这样做......这里有一些有趣的事情。如果我们这样做:

C1 编译器仅使用 !=

/*
 * run many iterations of this with :
 *  java -XX:+UnlockDiagnosticVMOptions  
 *       -XX:TieredStopAtLevel=1  
 *       "-XX:CompileCommand=print,com/so/BooleanCompare.compare"  
 *       com.so.BooleanCompare
 */
public static boolean compare(boolean left, boolean right) {
    return left != right;
}

我们得到:

  0x000000010d1b2bc7: push   %rbp
  0x000000010d1b2bc8: sub    $0x30,%rsp  ;*iload_0 {reexecute=0 rethrow=0 return_oop=0}
                                         ; - com.so.BooleanCompare::compare@0 (line 22)

  0x000000010d1b2bcc: cmp    %edx,%esi
  0x000000010d1b2bce: mov    $0x0,%eax
  0x000000010d1b2bd3: je     0x000000010d1b2bde
  0x000000010d1b2bd9: mov    $0x1,%eax
  0x000000010d1b2bde: and    $0x1,%eax
  0x000000010d1b2be1: add    $0x30,%rsp
  0x000000010d1b2be5: pop    %rbp

对我来说,这段代码有点明显:将 0 放入 eaxcompare (edx, esi) -> 如果不相等则将 1 放入 eax。返回eax &amp; 1

C1 编译器带 ^:

public static boolean compare(boolean left, boolean right) {
     return left ^ right;
}



  # parm0:    rsi       = boolean
  # parm1:    rdx       = boolean
  #           [sp+0x40]  (sp of caller)
  0x000000011326e5c0: mov    %eax,-0x14000(%rsp)
  0x000000011326e5c7: push   %rbp
  0x000000011326e5c8: sub    $0x30,%rsp   ;*iload_0 {reexecute=0 rethrow=0 return_oop=0}
                                          ; - com.so.BooleanCompare::compare@0 (line 22)

  0x000000011326e5cc: xor    %rdx,%rsi
  0x000000011326e5cf: and    $0x1,%esi
  0x000000011326e5d2: mov    %rsi,%rax
  0x000000011326e5d5: add    $0x30,%rsp
  0x000000011326e5d9: pop    %rbp

我真的不知道为什么这里需要and $0x1,%esi,否则这也很简单,我猜。

但如果我启用 C2 编译器,事情就会变得有趣得多。

/**
 * run with java
 * -XX:+UnlockDiagnosticVMOptions
 * -XX:CICompilerCount=2
 * -XX:-TieredCompilation
 * "-XX:CompileCommand=print,com/so/BooleanCompare.compare"
 * com.so.BooleanCompare
 */
public static boolean compare(boolean left, boolean right) {
    return left != right;
}



  # parm0:    rsi       = boolean
  # parm1:    rdx       = boolean
  #           [sp+0x20]  (sp of caller)
  0x000000011a2bbfa0: sub    $0x18,%rsp
  0x000000011a2bbfa7: mov    %rbp,0x10(%rsp)                

  0x000000011a2bbfac: xor    %r10d,%r10d
  0x000000011a2bbfaf: mov    $0x1,%eax
  0x000000011a2bbfb4: cmp    %edx,%esi
  0x000000011a2bbfb6: cmove  %r10d,%eax                     

  0x000000011a2bbfba: add    $0x10,%rsp
  0x000000011a2bbfbe: pop    %rbp

我什至没有看到经典的结语push ebp; mov ebp, esp; sub esp, x,而是一些非常不寻常的东西(至少对我来说),来自:

 sub    $0x18,%rsp
 mov    %rbp,0x10(%rsp)

 ....
 add    $0x10,%rsp
 pop    %rbp

再一次,比我多才多艺的人可以很有希望地解释一下。否则它就像生成的C1 的更好版本:

xor    %r10d,%r10d // put zero into r10d
mov    $0x1,%eax   // put 1 into eax
cmp    %edx,%esi   // compare edx and esi
cmove  %r10d,%eax  // conditionally move the contents of r10d into eax

由于分支预测,AFAIK cmp/cmovecmp/je 好 - 这至少是我读过的......

与 C2 编译器的 XOR:

public static boolean compare(boolean left, boolean right) {
    return left ^ right;
}



  0x000000010e6c9a20: sub    $0x18,%rsp
  0x000000010e6c9a27: mov    %rbp,0x10(%rsp)                

  0x000000010e6c9a2c: xor    %edx,%esi
  0x000000010e6c9a2e: mov    %esi,%eax
  0x000000010e6c9a30: and    $0x1,%eax
  0x000000010e6c9a33: add    $0x10,%rsp
  0x000000010e6c9a37: pop    %rbp

它确实看起来与C1 编译器生成的几乎相同。

【讨论】:

  • 你的更广泛的观点是正确的——差异会很小。但是您需要非常小心地尝试使用基准数字来证明这一点。微基准测试众所周知很困难,因为各种混杂因素都会影响您的测量,包括 CPU 本身完成的事情,例如分支预测、缓存等。除此之外,测量工具本身也有责任来影响结果。更不用说非常明显的可能性,即使用足够好的优化器,可以省略所有代码,这意味着您基本上什么都没有测试。
  • @CodyGray 我已经编辑了答案......如果你有时间解释一些我不明白的事情......谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-21
  • 2017-09-03
  • 2011-04-04
  • 1970-01-01
  • 2017-03-28
  • 1970-01-01
相关资源
最近更新 更多