有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

(p^q)和(p!=q)对于布尔人有什么有用的区别吗?

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的“criterium”基准测试工具对这两种方法的数十亿次调用进行了基准测试。虽然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

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

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


共 (1) 个答案

  1. # 1 楼答案

    好吧,我将很快提供CPU如何翻译这一点并更新帖子,但与此同时,您看到的waaaay差异太小,无法关注

    java中的字节码并不能表示一个方法的执行速度,有两个JIT编译器,一旦它们足够热,就会使这个方法看起来完全不同。另外javac已知在编译代码后只做很少的优化,真正的优化来自JIT

    我已经用JMH进行了一些测试,只使用C1编译器,或者用GraalVM替换C2,或者根本不使用JIT。。。(下面有很多测试代码,您可以跳过它,只需查看结果,这是使用jdk-12btw完成的)。这段代码使用的是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 compiler only with !=

    /*
     * 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 & 1

    C1 compiler with ^:

    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,否则我想这也相当简单

    But if I enable C2 compiler, things are a lot more interesting.

    /**
     * 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
    

    因为分支预测,AFAIKcmp/cmovecmp/je好——至少我读到了这一点

    XOR with C2 compiler:

    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几乎一样