У меня есть метод Java, который возвращает либо 0, либо 1. Могу ли я заставить его вернуть логическое значение без создания инструкции ветви?
На уровне байтового кода логическое значение 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
Я согласен, что код должен быть читабельным (именно поэтому я хочу, чтобы производительность версии branchless int с помощью правильный логический интерфейс), и в большинстве случаев этот уровень оптимизации не гарантирован. Однако в этом случае был получен выигрыш в 10%, даже когда рассматриваемый метод даже не учитывает большую часть кода.
Так что, возможно, мы имеем здесь случай, когда HotSpot может быть осведомлен об этом шаблоне и генерировать лучший код.
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И работает без проблем. Но, как уже было сказано, это всего лишь упражнение, не имеющее большой практической ценности.
Comments