我有一个返回0或1的Java方法。我可以让它返回一个布尔值而不生成分支指令吗?

时间:2018-01-30 16:55:02

标签: java bytecode jit

在字节代码级别,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

返回布尔值的方法要大得多,并且包含一个分支,因此如果运行的机器代码是等效的,那么它就不太理想了。

HotSpot JVM是否知道布尔版本可以优化为无网格机器代码?有没有办法诱骗Java使用基于int的字节代码来返回一个布尔值的方法(例如使用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

我同意代码应该是可读的(这就是为什么我希望无法使用具有正确布尔接口的无分支int版本的性能),并且大多数情况下这种优化级别是不合理的。然而,在这种情况下,即使有问题的方法甚至不占代码的大部分,也有10%的收益。

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

1 个答案:

答案 0 :(得分:1)

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

请注意,只有在boolean(包括声明返回return的方法的boolean语句)的显式赋值时,才会将显式转换为零或一次。当表达式是条件或复合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操作之前未转换为零或一个,只是最终结果。

同样地

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的知识返回零或一个比一个更好的知识。

所以当我们谈论的时候,例如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

并且没有问题。但正如所说的那样,这只是一种练习,而不是很有实际价值。