Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

java

bytecode

jit

At the byte code level a Java boolean is represented as either 0 or 1. I have an expression that results in 0 or 1 but it is computed with the int type. A simple example would be:

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

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

The byte code for the above methods looks like this:

  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

The method that returns a boolean is much larger and contains a branch so it is less optimal if the machine code that runs is equivalent.

Will the HotSpot JVM know that the boolean version can be optimized to branchless machine code? Is there a way to trick Java into using the int-based byte code for a method that returns a boolean (e.g. using ASM) ?

EDIT: Many have suggested this isn't worth worrying about, and in general I agree. However I did created this micro benchmark and ran it with jmh and noticed an improvement with the int version of around 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

I agree the code should be readable (that's why I want the performance of the branchless int version with the proper boolean interface), and most of the time this level of optimization is not warranted. However in this case there was a 10% gain even when the method in question doesn't even account for a majority of the code.

So perhaps what we have here is a case where HotSpot can be made aware of this pattern and generate better code.

like image 495
swpalmer Avatar asked Jan 30 '18 16:01

swpalmer


People also ask

Can a method return a Boolean Java?

In Java, you must declare a method of the boolean type in order for it to return a boolean value. The boolean method will return the boolean value, true or false. You can either return the variable containing a boolean value or use conditional statements to decide the returned value.

How to make a method return a value Java?

You declare a method's return type in its method declaration. Within the body of the method, you use the return statement to return the value. Any method declared void doesn't return a value. It does not need to contain a return statement, but it may do so.

Can you return nothing in Java?

In Java, a null value can be assigned to an object reference of any type to indicate that it points to nothing. The compiler assigns null to any uninitialized static and instance members of reference type. In the absence of a constructor, the getArticles() and getName() methods will return a null reference.

How do you define a boolean in Java?

To display Boolean type, firstly take two variables and declare them as boolean. val1 = true; Now, use if statement to check and display the Boolean true value. if(val1) System.


1 Answers

First of all, 10% is not a speed difference that is worth any effort.

Note that explicit conversions to zero or one only happen when there is an explicit assignment to boolean (which includes return statements of methods declared to return boolean). When the expression is part of a conditional or a compound boolean expression, this will not happen, e.g.

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

compiles to

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

As you see, the two expressions are not converted to zero or one before the and operation, only the final result.

Likewise

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

compiles to

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

not bearing any conversion to zero or one.

(Note that comparing with zero here utilizes the knowledge about i&1 returning zero or one better than comparing with one).


So when we’re talking about, e.g. 0.01% of an actual application code (or even less) and assume a 10% speedup of that particular code, we can expect an overall speed improvement of 0.001% (or even less).


Still, just for fun or as a small code compression feature (maybe as part of a more general code compression or byte code obfuscation), here, an ASM based solution:

To make the transformation easier, we define a place-holder method, i2b performing an int to boolean transformation and invoke it at the intended place(s). The transformator simply removes both, the method declaration and its invocations:

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();
        }
    }
}

The transformed method looks like

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

and works without problems. But as said, that’s just as an exercise, not of much practical worth.

like image 83
Holger Avatar answered Oct 29 '22 15:10

Holger