在字节代码级别,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了解这种模式并生成更好的代码。
答案 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
执行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
并且没有问题。但正如所说的那样,这只是一种练习,而不是很有实际价值。