У меня есть метод 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 может быть осведомлен об этом шаблоне и генерировать лучший код.

489   1  

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

    Ничего не найдено.