Java-программа работает медленнее, когда закомментирован код, который никогда не выполняется
Описание проблемы
Я столкнулся с необычным поведением в одной из своих 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 ответ(ов)
В контексте вашего вопроса, код, который вы комментируете, влияет на то, как обрабатывается инлайнинг. Если функция 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
Скорее всего, длинная версия байт-кода, которая не инлайнится, будет вызывать проблемы с предсказанием ветвлений и кэшированием.
Полный ответ является комбинацией ответов 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);
}
Инициализация ArrayList в одну строчку
Почему нет ConcurrentHashSet, если есть ConcurrentHashMap?
Как объявить массив в одну строку?
Загрузка JDK Java на Linux через wget приводит к отображению страницы лицензии вместо установки
Создание репозитория Spring без сущности