我有一个 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
执行 int
到 boolean
的转换并在预期的位置调用它。转换器简单地删除了方法声明及其调用:
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
并且工作没有问题。不过如前所述,那只是练习,没有太大的实际价值。
在字节码级别,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
执行 int
到 boolean
的转换并在预期的位置调用它。转换器简单地删除了方法声明及其调用:
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
并且工作没有问题。不过如前所述,那只是练习,没有太大的实际价值。