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

swp*_*mer 6 java jit bytecode

在字节代码级别,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;
}
Run Code Online (Sandbox Code Playgroud)

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

  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
Run Code Online (Sandbox Code Playgroud)

返回布尔值的方法要大得多,并且包含一个分支,因此如果运行的机器代码是等效的,则它不是最佳的.

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
Run Code Online (Sandbox Code Playgroud)

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

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

Hol*_*ger 1

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

\n\n

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

\n\n
static boolean isOddAndShort(int i) {\n    return (i&1)!=0 && (i>>>16)==0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

编译为

\n\n
static boolean isOddAndShort(int);\ndescriptor: (I)Z\nflags: ACC_STATIC\nCode:\n  stack=2, locals=1, args_size=1\n     0: iload_0\n     1: iconst_1\n     2: iand\n     3: ifeq          17\n     6: iload_0\n     7: bipush        16\n     9: iushr\n    10: ifne          17\n    13: iconst_1\n    14: goto          18\n    17: iconst_0\n    18: ireturn\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如你所看到的,这两个表达式在运算之前并没有转换为0或1and,只是最终的结果。

\n\n

同样地

\n\n
static void evenOrOdd(int i) {\n    System.out.println((i&1)!=0? "odd": "even");\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

编译为

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

不承担任何到0 或 1 的转换。

\n\n

i&1(请注意,此处与零比较比与一比较更好地利用了有关返回零或一的知识)。

\n\n
\n\n

因此,当我们\xe2\x80\x99 谈论实际应用程序代码的 0.01%(甚至更少)并假设该特定代码的加速率为 10% 时,我们可以预期总体速度提高 0.001%(甚至更少) )。

\n\n
\n\n

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

\n\n

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

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

转换后的方法看起来像

\n\n
public static boolean isOdd(int);\ndescriptor: (I)Z\nflags: ACC_PUBLIC, ACC_STATIC\nCode:\n  stack=2, locals=1, args_size=1\n     0: iload_0\n     1: iconst_1\n     2: iand\n     3: ireturn\n
Run Code Online (Sandbox Code Playgroud)\n\n

并且工作没有问题。但正如所说,\xe2\x80\x99 只是作为练习,没有多大实际价值。

\n