0

Java-программа работает медленнее, когда закомментирован код, который никогда не выполняется

4

Описание проблемы

Я столкнулся с необычным поведением в одной из своих Java-программ. Я постарался минимизировать код, чтобы сохранить возможность воспроизведения этой аномалии. Полный код приведен ниже.

public class StrangeBehaviour {

    static boolean recursionFlag = true;

    public static void main(String[] args) {
        long startTime = System.nanoTime();
        for (int i = 0; i < 10000; i ++) {
            functionA(6, 0);
        }
        long endTime = System.nanoTime();
        System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
    }

    static boolean functionA(int recursionDepth, int recursionSwitch) {
        if (recursionDepth == 0) { return true; }
        return functionB(recursionDepth, recursionSwitch);
    }

    static boolean functionB(int recursionDepth, int recursionSwitch) {
        for (int i = 0; i < 16; i++) {
            if (StrangeBehaviour.recursionFlag) {
                if (recursionSwitch == 0) {
                    if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
                } else {
                    if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
                }
            } else {
                // Этот блок кода никогда не выполняется.
                // Но если закомментировать хотя бы одну из строк ниже, программа начинает работать медленнее!
                System.out.println("...");
            }
        }
        return false;
    }
}

У меня есть две функции, functionA() и functionB(), которые рекурсивно вызывают друг друга. Обе функции принимают параметр recursionDepth, который контролирует завершение рекурсии. Функция functionA() вызывает functionB() максимум один раз с неизменным recursionDepth. Функция functionB() вызывает functionA() 16 раз с recursionDepth - 1. Рекурсия завершается, когда functionA() вызывается с recursionDepth равным 0.

Функция functionB() содержит блок кода с несколькими вызовами System.out.println(). Этот блок никогда не выполняется, так как вход в него контролируется переменной boolean recursionFlag, которая установлена в true и никогда не изменяется в процессе выполнения программы. Однако комментирование даже одного из вызовов println() значительно замедляет выполнение программы. На моем компьютере время выполнения составляет менее 0,2 секунды с присутствующими всеми вызовами println(), и более 2 секунд, если один из вызовов закомментирован.

Что может вызывать это поведение? Мое единственное предположение заключается в том, что происходит какая-то наивная оптимизация компилятора, которая срабатывает в зависимости от параметров, связанных с длиной блока кода (или с количеством вызовов функций и т.д.). Буду признателен за любые идеи и пояснения!

Дополнение: Я использую JDK 1.8.

2 ответ(ов)

0

В контексте вашего вопроса, код, который вы комментируете, влияет на то, как обрабатывается инлайнинг. Если функция functionB становится длиннее или занимает больше байт (то есть содержит больше инструкций байт-кода), она не будет инлайневаться в functionA.

Как указал @J3D1, он смог использовать параметры JVM для отключения инлайнинга для functionB(): -XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBehaviour::functionB. Это, по-видимому, устраняет задержку для короткой функции.

С помощью опций JVM можно отобразить инлайнинг, используя:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

Большая версия не будет инлайневаться

@ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 21   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 35   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large

Короткая версия попытается инлайнить functionB, вызвав несколько дальнейших попыток

@ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
 @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
 @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 21   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
@ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
@ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
    @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep

Скорее всего, длинная версия байт-кода, которая не инлайнится, будет вызывать проблемы с предсказанием ветвлений и кэшированием.

0

Полный ответ является комбинацией ответов k5_ и Tony.

Код, который опубликовал автор вопроса, пропускает цикл разогрева, чтобы запустить компиляцию HotSpot перед выполнением бенчмарка; именно поэтому на моем компьютере наблюдается 10-кратное (когда включены команды вывода) ускорение, которое включает как время, затраченное HotSpot на компиляцию байт-кода в инструкции процессора, так и фактическое выполнение этих инструкций.

Если я добавлю отдельный цикл разогрева перед циклом замеров, то скорость возрастает только в 2.5 раза с командами вывода.

Это говорит о том, что как компиляция HotSpot/JIT занимает больше времени, когда метод инлайнится (как объяснил Tony), так и выполнение кода занимает больше времени, вероятно, из-за ухудшения производительности кэша или предсказания ветвлений/конвейеризации, как показал k5_.

public static void main(String[] args) {
    // Добавленный цикл разогрева перед циклом замеров
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }

    long startTime = System.nanoTime();
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }
    long endTime = System.nanoTime();
    System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь