JEP 515: Ahead-of-Time Method Profiling
Профилирование методов в режиме Ahead-of-Time (заблаговременная компиляция и подготовка)
| Автор | Igor Veresov & John Rose |
| Ответственный | John Rose |
| Тип | Feature |
| Область | Implementation |
| Статус | Closed / Delivered |
| Выпуск | 25 |
| Компонент | hotspot / compiler |
| Обсуждение | leyden dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 483: Ahead-of-Time Class Loading & Linking |
| JEP 544: Ahead-of-Time Code Compilation | |
| Рецензенты | Alex Buckley, Dan Heidinga, Vladimir Kozlov |
| Одобрен | Vladimir Kozlov |
| Создан | 2024/02/01 20:40 |
| Обновлён | 2026/06/09 21:55 |
| Задача | 8325147 |
Аннотация
Сократить время прогрева: профили выполнения методов, собранные при предыдущем запуске приложения, должны быть доступны сразу при старте HotSpot Java Virtual Machine. Так JIT-компилятор сможет генерировать машинный код сразу при запуске приложения и не будет ждать, пока соберутся профили.
Цели
-
Помочь приложениям прогреваться быстрее: перенести сбор начальных профилей выполнения методов из рабочих запусков в обучающие и передавать профили через AOT Cache (кэш, подготовленный заранее).
-
Не требовать никаких изменений в коде приложений, библиотек или фреймворков.
-
Не вводить никаких новых ограничений на выполнение приложений.
-
Не вводить новые сценарии работы с AOT, а использовать существующие команды создания AOT Cache.
Мотивация
Чтобы действительно узнать, что делает приложение, его нужно запустить.
Мы можем сделать простые выводы о поведении приложения, изучив его исходный код или class-файлы, но не будем уверены в том, как оно взаимодействует с чрезвычайно динамичной платформой Java. Одна из причин этой неопределённости в том, что при отсутствии модификаторов final или sealed от любого класса в любой момент можно унаследовать подкласс, поэтому метод может много раз вызываться, а затем быть переопределён и больше никогда не вызываться. Другая причина в том, что в ответ на внешний ввод могут загружаться новые классы, которые расширяют поведение приложения так, как не мог предсказать даже его автор. Сложность программы всегда может свести статический анализ на нет.
При выполнении приложения JVM может определить, какие методы выполняют важную работу и как именно они это делают. Чтобы приложение достигло пиковой производительности, JIT-компилятор JVM (just-in-time compiler, JIT) должен найти непредсказуемый набор горячих методов, т. е. тех, которые потребляют больше всего процессорного времени, и скомпилировать их байт-код в машинный код. (Отсюда и название «HotSpot JVM».) Поскольку прошлое поведение приложения отлично предсказывает его будущее поведение, сводка прошлого поведения позволяет сосредоточить усилия JVM по компиляции на действительно важном коде.
Начиная с JDK 1.2, HotSpot JVM автоматически собирает такую сводку в виде профилей. Для каждого метода профиль подсчитывает множество полезных событий, например, сколько раз выполняются его инструкции байт-кода и какие типы объектов при этом встречаются. Имея достаточно данных профиля, JVM получает статистическую основу, чтобы предсказать будущее поведение метода и, следовательно, сгенерировать для этого метода оптимизированный код. Профили позволяют JVM как оптимизировать горячие методы, так и не оптимизировать холодные; для пиковой производительности необходимо и то и другое.
К сожалению, здесь возникает проблема курицы и яйца: приложение не может достичь пиковой производительности, пока не предсказано поведение его методов, а поведение методов нельзя предсказать, пока приложение не проработает достаточно долго.
Сейчас JVM решает эту проблему, выделяя часть ресурсов на сбор профилей в начале работы приложения. В этот период прогрева приложение работает медленнее, пока JIT не скомпилирует горячие методы в машинный код. После прогрева компилировать методы больше не нужно, если только приложение не изменит характер своего поведения, что запустит новый период прогрева.
Мы можем сократить время прогрева, собирая профили ещё раньше, в обучающем запуске приложения. Так работа по профилированию и предсказанию поведения выносится за пределы рабочего времени жизни приложения. В результате время прогрева приложения в рабочей среде будет определяться только затратами на JIT-компиляцию, и приложение сможет быстрее достичь пиковой производительности.
Описание
Мы расширяем AOT Cache, представленный в JEP 483, чтобы собирать профили методов во время обучающих запусков. Подобно тому как AOT Cache сейчас хранит классы, которые иначе JVM пришлось бы загружать и связывать при запуске, теперь AOT Cache также хранит профили методов, которые иначе JVM пришлось бы собирать в начале работы приложения. Соответственно, при рабочих запусках приложение и стартует быстрее, и быстрее достигает пиковой производительности.
Профили, закэшированные во время обучающих запусков, не мешают дополнительному профилированию во время рабочих запусков. Это критически важно, поскольку поведение приложения в рабочей среде может отличаться от того, что наблюдалось при обучении. Даже при наличии закэшированных профилей HotSpot JVM продолжает профилировать и оптимизировать приложение во время его работы, объединяя преимущества AOT-профилей, профилирования на лету и JIT-компиляции. В итоге закэшированные профили приводят к тому, что JIT начинает работу раньше и действует точнее, используя профили для оптимизации горячих методов, так что период прогрева приложения сокращается. Задачи JIT по своей природе параллельны, поэтому при достаточных аппаратных ресурсах реальное время прогрева может быть коротким.
Например, вот программа, которая, хотя и короткая, использует Stream API и поэтому вызывает загрузку почти 900 классов JDK. Около 30 горячих методов компилируются на самом высоком уровне оптимизации:
import java.util.*;
import java.util.stream.*;
public class HelloStreamWarmup {
static String greeting(int n) {
var words = List.of("Hello", "" + n, "world!");
return words.stream()
.filter(w -> !w.contains("0"))
.collect(Collectors.joining(", "));
}
public static void main(String... args) {
for (int i = 0; i < 100_000; i++)
greeting(i);
System.out.println(greeting(0)); // "Hello, world!"
}
}
С AOT Cache без профилей эта программа выполняется за 90 миллисекунд. После сбора профилей в AOT Cache она выполняется за 73 миллисекунды — улучшение на 19 %. AOT Cache с профилями занимает дополнительно 250 килобайт, примерно на 2,5 % больше, чем AOT Cache без профилей.
У такой короткой программы период прогрева и так короткий, но с закэшированными профилями прогрев проходит ещё быстрее благодаря своевременной и точной работе JIT. Более сложные и дольше работающие программы, вероятно, по той же причине тоже будут прогреваться быстрее.
Альтернативы
Если приложение настолько предсказуемо, что его горячие методы можно скомпилировать в машинный код заранее и это позволяет ему достичь пиковой производительности без дальнейшей работы JIT, то такой AOT-код предпочтительнее кэширования профилей. Мы планируем реализовать AOT-компиляцию в дальнейшей работе.
Однако многим приложениям выгодно сочетание AOT-компиляции и JIT-компиляции, поскольку их поведение не может быть точно предсказано AOT-компилятором. Таким образом, закэшированные профили и закэшированный AOT-код не противоречат друг другу, а будут дополнять друг друга и обеспечат наилучшую производительность для самых разных приложений. Частичное AOT-решение, при котором приемлемый AOT-код постепенно заменяется лучше оптимизированным JIT-кодом, в конечном счёте, по-видимому, окажется наилучшим. Поначалу JIT может не мешать приложению и не торопясь доводить итоговый код до идеала на основе самой свежей информации профилирования.
Тестирование
-
Мы создадим новые модульные тесты для этой возможности.
-
Мы запустим существующие тесты AOT Cache с включённой возможностью и убедимся, что они проходят.
Риски и допущения
Новых рисков, помимо уже отмеченных в JEP 483, нет.
Базовое допущение AOT Cache остаётся в силе: предполагается, что обучающий запуск — хороший источник наблюдений, которые, будучи переданы через AOT Cache в рабочий запуск, улучшат производительность этого рабочего запуска.