我有一个 Java 方法 return 要么是 0 要么是 1。我可以在不生成分支指令的情况下将它设为 return 布尔值吗?

I have a Java method that returns either 0 or 1. Can I make it return a boolean without generating a branch instruction?

在字节码级别,Java 布尔值表示为 0 或 1。我有一个结果为 0 或 1 的表达式,但它是使用 int 类型计算的。一个简单的例子是:

public static int isOdd_A(int value) {
    return value & 1;
}

public static boolean isOdd_B(int value) {
    return (value & 1) == 1;
}

上述方法的字节码如下所示:

  public static int isOdd_A(int);
    descriptor: (I)I
    Code:
       0: iload_0
       1: iconst_1
       2: iand
       3: ireturn

  public static boolean isOdd_B(int);
    descriptor: (I)Z
    Code:
       0: iload_0
       1: iconst_1
       2: iand
       3: iconst_1
       4: if_icmpne     11
       7: iconst_1
       8: goto          12
      11: iconst_0
      12: ireturn

returns一个boolean更大并且包含一个b运行ch的方法,所以如果运行的机器代码是等价的,它不是最优的。

HotSpot JVM 会知道布尔版本可以优化为 b运行chless 机器代码吗?有没有办法欺骗 Java 将基于 int 的字节码用于 returns 布尔值(例如使用 ASM)的方法?

编辑: 许多人认为这不值得担心,总的来说我同意。然而,我确实创建了这个微型基准测试,并使用 jmh 运行 并注意到 int 版本的改进大约为 10%:

@Benchmark
public int countOddA() {
    int odds = 0;
    for (int n : numbers)
        if (Test.isOdd_A(n) == 1)
            odds++;
    return odds;
}
@Benchmark
public int countOddB() {
    int odds = 0;
    for (int n : numbers)
        if(Test.isOdd_B(n))
            odds++;
    return odds;
}

Benchmark                Mode  Cnt      Score    Error  Units
OddBenchmark.countOddA  thrpt  100  18393.818 ± 83.992  ops/s
OddBenchmark.countOddB  thrpt  100  16689.038 ± 90.182  ops/s

我同意代码应该是可读的(这就是为什么我想要具有适当布尔接口的 b运行chless int 版本的性能),并且大多数时候这种优化级别不是战争运行泰德。然而,在这种情况下,即使所讨论的方法甚至不占代码的大部分,也有 10% 的收益。

所以也许我们这里有一个案例,可以让 HotSpot 了解这种模式并生成更好的代码。

首先,10%不是一个值得付出任何努力的速度差异。

请注意,仅当对 boolean 进行显式赋值时才会显式转换为零或一(其中包括声明为 return boolean 的方法的 return 语句).当表达式是条件或复合 boolean 表达式的一部分时,这不会发生,例如

static boolean isOddAndShort(int i) {
    return (i&1)!=0 && (i>>>16)==0;
}

编译为

static boolean isOddAndShort(int);
descriptor: (I)Z
flags: ACC_STATIC
Code:
  stack=2, locals=1, args_size=1
     0: iload_0
     1: iconst_1
     2: iand
     3: ifeq          17
     6: iload_0
     7: bipush        16
     9: iushr
    10: ifne          17
    13: iconst_1
    14: goto          18
    17: iconst_0
    18: ireturn

如你所见,在and操作之前,这两个表达式并没有被转换为0或1,只是最后的结果。

同样

static void evenOrOdd(int i) {
    System.out.println((i&1)!=0? "odd": "even");
}

编译为

static void evenOrOdd(int);
descriptor: (I)V
flags: ACC_STATIC
Code:
  stack=3, locals=1, args_size=1
     0: getstatic     #2        // Field java/lang/System.out:Ljava/io/PrintStream;
     3: iload_0
     4: iconst_1
     5: iand
     6: ifeq          14
     9: ldc           #3        // String odd
    11: goto          16
    14: ldc           #4        // String even
    16: invokevirtual #5        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    19: return

不承担任何转换为​​ 零或一

(请注意,此处与零比较利用了 i&1 return 零或一比与一比较更好的知识。


所以当我们谈论,例如实际应用程序代码的 0.01%(或更少)并假设该特定代码加速 10%,我们可以预期整体速度提高 0.001%(或更少)。


不过,只是为了好玩或作为一个小的代码压缩功能(可能作为更通用的代码压缩或字节码混淆的一部分),这里是一个基于 ASM 的解决方案:

为了使转换更容易,我们定义了一个占位符方法,i2b 执行 intboolean 的转换并在预期的位置调用它。转换器简单地删除了方法声明及其调用:

public class Example {
    private static boolean i2b(int i) {
        return i!=0;
    }
    public static boolean isOdd(int i) {
        return i2b(i&1);
    }
    public static void run() {
        for(int i=0; i<10; i++)
            System.out.println(i+": "+(isOdd(i)? "odd": "even"));
    }
}
public class Int2Bool {
    public static void main(String[] args) throws IOException {
        String clName = Example.class.getName();
        ClassReader cr = new ClassReader(clName);
        ClassWriter cw = new ClassWriter(cr, 0);
        cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                if(name.equals("i2b") && desc.equals("(I)Z")) return null;
                return new MethodVisitor(Opcodes.ASM5, super.visitMethod(access, name, desc, signature, exceptions)) {
                    @Override
                    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
                        if(opcode == Opcodes.INVOKESTATIC && name.equals("i2b") &&  desc.equals("(I)Z"))
                            return;
                        super.visitMethodInsn(opcode, owner, name, desc, itf);
                    }
                };
            }
        }, 0);
        byte[] code = cw.toByteArray();
        if(writeBack(clName, code))
            Example.run();
        else
            runDynamically(clName, code);
    }
    private static boolean writeBack(String clName, byte[] code) {
        URL u = Int2Bool.class.getResource("/"+clName.replace('.', '/')+".class");
        if(u==null || !u.getProtocol().equals("file")) return false;
        try {
            Files.write(Paths.get(u.toURI()), code, StandardOpenOption.TRUNCATE_EXISTING);
            return true;
        } catch(IOException|URISyntaxException ex) {
            ex.printStackTrace();
            return false;
        }
    }

    private static void runDynamically(String clName, byte[] code) {
        // example run
        Class<?> rtClass = new ClassLoader() {
            Class<?> get() { return defineClass(clName, code, 0, code.length); }
        }.get();
        try {
            rtClass.getMethod("run").invoke(null);
        } catch (ReflectiveOperationException ex) {
            ex.printStackTrace();
        }
    }
}

转换后的方法看起来像

public static boolean isOdd(int);
descriptor: (I)Z
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=1, args_size=1
     0: iload_0
     1: iconst_1
     2: iand
     3: ireturn

并且工作没有问题。不过如前所述,那只是练习,没有太大的实际价值。