JEP draft: Instruction Issue Cache Hardware Accommodation
Учёт аппаратных ограничений кэшей выдачи инструкций
| Ответственный | Paul Hohensee |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | hotspot / compiler |
| Трудоёмкость | L |
| Длительность | L |
| Создан | 2021/12/22 21:32 |
| Обновлён | 2024/04/29 14:45 |
| Задача | 8279184 |
Аннотация
В этом JEP рассматриваются проекты, которые могут смягчить негативное влияние аппаратных ограничений на выдачу инструкций (instruction issue hardware limitations, IIHL) на производительность сгенерированного кода. К такому оборудованию относятся, в частности, встроенные в процессор кэши памяти, ITLB (Instruction Translation Lookaside Buffers, буферы ассоциативной трансляции для инструкций) и BTP (Branch Target Predictors, предсказатели целевых адресов переходов). Рассматриваемые меры включают:
- размещение разных видов сгенерированного кода в соответственно разных диапазонах виртуальных адресов (см. JEP-197: Segmented Code Cache (http://openjdk.java.net/jeps/197));
- чтобы свести к минимуму промахи ITLB, отображение сгенерированного кода по умолчанию на огромные страницы (huge pages; 2 МБ на x64) в тех случаях, когда ОС можно настроить так, чтобы избежать задержек при их слиянии;
- разделение nmethod путём размещения их часто используемых частей (например, сгенерированного кода) отдельно от остального содержимого (см. JDK-7072317: move metadata from CodeCache (https://bugs.openjdk.java.net/browse/JDK-7072317)); и
- периодическое совместное размещение (то есть уплотнение) горячего кода, чтобы сократить число и размер содержащих его диапазонов виртуальных адресов.
Основное внимание в этом JEP уделено уплотнению, но в него также включено несколько предложений других проектов. Совместное размещение возможно как в сегментированном, так и в несегментированном кэше кода. Периодическое повторное совместное размещение необходимо, чтобы адаптироваться к сменам фаз работы приложения.
Если в реализации окажутся платформенно-зависимые аспекты, начальными целевыми платформами будут linux-x64 и linux-aarch64.
Цели
Снизить негативное влияние IIHL на производительность Java-приложений.
Что не является целью
Критерии успеха
- Сокращение времени выполнения приложений.
Мотивация
IIHL ограничивают число и размер диапазонов виртуальных адресов, которые могут быть отображены одновременно, поэтому совместное размещение горячего кода приложения может повысить производительность. Совместное размещение горячего кода на основе данных профилирования, а не событий переполнения счётчиков, должно сократить как промахи из-за нехватки ёмкости, так и промахи из-за конфликтов, а также вызванные ими потери на промахах. Примеры — задержки на выборку инструкций, повторное заполнение ITLB, опустошение конвейера инструкций и повторное заполнение записей буфера целевых адресов переходов.
Сегментированный кэш кода, JEP-197 (https://openjdk.java.net/jeps/197), был создан отчасти для этой цели. Однако в его архитектуре не учитывается, что событие переполнения счётчика вызовов метода и/или счётчика обратных переходов цикла само по себе не связано с тем, насколько горячими на самом деле являются метод и/или цикл. Переполнение счётчика служит мерой «горячести» только в сочетании со временем, за которое счётчик переполнился, и даже тогда это лишь одна точка данных профиля. В результате куча кода заполняется примерно в хронологическом порядке переполнения счётчиков, из-за чего горячий в установившемся режиме код часто оказывается разбросанным по куче кода без возможности его уплотнить. Особенно это проявляется, когда запуск приложения требует интенсивных вычислений: большая часть кода запуска в установившемся режиме холодная, но оказывается перемежающейся с горячим кодом установившегося режима.
Описание
Оборудование выдачи инструкций может быть хрупким в том смысле, что оно работает оптимально, когда исполняемый код ограничен одним или несколькими узкими диапазонами виртуальных адресов. Текущее значение ReservedCodeCache по умолчанию, 240 МБ, велико, чтобы вместить сегментированный кэш кода и систему многоуровневой компиляции. Сегментированный кэш кода делит кэш на три кучи кода: по одной для кода, не являющегося nmethod (шаблонный интерпретатор и т. п.), для сгенерированных компилятором профилируемых nmethod и для сгенерированных компилятором непрофилируемых/оптимизированных nmethod. Более подробное описание см. в JEP-197 (https://openjdk.java.net/jeps/197). Согласно принципу 80/20, общий размер горячего кода установившегося режима обычно относительно невелик, но содержащие его nmethod из-за порядка многоуровневой компиляции оказываются разбросанными по адресному пространству кучи кода, что, в свою очередь, упирается в IIHL. Эксперименты с меньшим (64 МБ) несегментированным кэшем кода и отключённой многоуровневой компиляцией показали, что это влияние можно частично смягчить, группируя горячие nmethod вместе, то есть более удобным для оборудования способом.
Другой подход — разделить nmethod на часто используемую («код») и редко используемую («метаданные») части и размещать каждый набор частей вместе. Код включает, в частности, сгенерированный код и пул констант. Метаданные включают, в частности, данные о релокациях, данные о зависимостях и oopmaps. Метаданные можно размещать полностью вне кэша кода, либо код можно размещать от начала кучи кода вверх, а метаданные — от конца кучи кода вниз. Задача в JBS, JDK-7072317: move metadata from CodeCache (https://bugs.openjdk.java.net/browse/JDK-7072317), предполагает полное удаление метаданных из кэша кода.
Более сложным и основательным решением была бы адаптация к изменениям в поведении приложения путём отслеживания выполнения nmethod и периодического уплотнения горячего кода. Уплотнять метаданные не строго необходимо, но это может быть желательно для смягчения фрагментации кучи кода. Если целевое место в куче кода уже освобождено, перемещать код nmethod можно конкурентно: создать перемещённую копию nmethod, исправить внешние ссылки на её точки входа, а затем применить к исходному nmethod, на который больше нет ссылок, существующие протоколы инвалидации кода и восстановления nmethod.
Ожидается, что основная часть работы будет состоять в обнаружении горячего кода, принятии решения о том, когда его уплотнять, и эффективной перестановке/уплотнении горячего кода внутри кучи кода. Уплотнение должно выполняться полностью или почти полностью конкурентно с выполнением приложения. Профилирование горячего кода с помощью трассировки влечёт накладные расходы на время выполнения, поэтому, вероятно, предпочтение будет отдано механизму сэмплирования. В JFR (JDK Flight Recorder) есть сэмплер, который можно было бы использовать для этой цели, но его накладные расходы необходимо тщательно контролировать.
Перестроить кучу кода так, чтобы стало возможно уплотнение, можно, просто инвалидировав мешающий код и позволив ему позже перекомпилироваться. При более сложном подходе такой код перемещался бы в другое место, но это может потребовать больше места в куче кода, поскольку обе копии перемещаемого кода одновременно занимали бы место в куче кода.
Читатели, имеющие опыт реализации сборщиков мусора, заметят сходства и аналогии между управлением кэшем кода и преимущественно или полностью конкурентными сборщиками. Реализация этого JEP должна опираться на механизмы и реализации таких сборщиков.
Возможные проекты:
- Разделить nmethod на часто и редко используемые части и размещать их отдельно, как описано выше. См. JDK-7072317 (https://bugs.openjdk.java.net/browse/JDK-7072317).
- Перенести механизм профилирования методов JFR на основе сэмплирования в ядро JVM. Полученную общую реализацию использовали бы и управление кэшем кода, и JFR. Заменить существующее отслеживание холодных nmethod этой общей реализацией. В качестве потока сэмплирования можно было бы использовать поток Watcher или Low Memory: отдельный поток сэмплирования позволил бы избежать их перегрузки.
- Придумать и реализовать политику, определяющую, когда следует выполнять уплотнение. Если существуют аппаратные счётчики, регистрирующие промахи кэша выдачи инструкций, политика могла бы их использовать. В этом случае понадобился бы асинхронный механизм, для которого следует завести отдельный JEP. Для целей этого JEP можно использовать чисто программное решение.
- Чтобы переместить nmethod, нужно удалить старую копию, а для этого требуется быстрый способ обнаруживать неактивные nmethod. Придумать и реализовать менее затратный по времени способ это делать. Текущая зависимость от полных циклов маркировки означает, что обнаружение обычно происходит медленно и нечасто.
- Первый этап уплотнения: зарезервировать фиксированный объём пространства кучи кода для горячих nmethod и перемещать их в этот диапазон адресов. Можно использовать подход с пространствами выживших from/to, чтобы не обрабатывать случай фрагментации из-за горячих nmethod, которые уже занимают целевой диапазон адресов.
- Второй этап уплотнения: определить целевые адреса для уплотняемых nmethod (возможно, начало кучи кода, но, может быть, лучше выбрать диапазон адресов, который уже содержит многие из них), деоптимизировать негорячие nmethod, занимающие целевой диапазон адресов, и перемещать в него горячие nmethod по мере освобождения места. Обработать особый случай горячих nmethod, которые уже находятся в целевом диапазоне адресов.
- Третий этап уплотнения: вместо деоптимизации nmethod, занимающих целевой диапазон адресов, перемещать их в другое место кэша кода.
- Компиляторы могли бы распознавать сильно связные области графа вызовов программы и генерировать несколько nmethod, код которых статически уплотнён вместе. Пример системы, которая статически уплотняет горячий код с использованием обратной связи от профилирования, — Automated-Hot-Text-and-Huge-Pages-An-Easy-to-adopt-Solution-Towards-High-Performing-Services компании Facebook.
Из-за процессорного времени, затрачиваемого механизмом совместного размещения, возможно снижение производительности, поэтому должна быть возможность вернуться к существующей реализации управления кэшем кода. Может оказаться желательным иметь несколько политик управления кэшем кода, рассчитанных на разные цели производительности, так же как существует несколько политик сборки мусора. Формальный механизм политик управления кэшем кода стал бы предметом другого JEP.
Кэш кода — центральный компонент JVM, поэтому эти изменения затронут и другие компоненты, в том числе, но не только
- JMX: события (например, пороги использования), связанные с пулом памяти кэша кода,
- Java Flight Recorder (JFR): события, связанные с кэшем кода, и
- Serviceability Agent: Java-интерфейс к внутреннему устройству кэша кода.
Альтернативы
Существующий сегментированный кэш кода был создан для достижения многих из тех же целей, что и этот JEP. Например, размещение кода, сгенерированного серверным компилятором, в отдельной куче кода сокращает число диапазонов адресов, занятых предположительно горячим кодом. В сегментированном кэше кода предположительно тёплый/холодный код, сгенерированный клиентским компилятором, не перемежается с кодом, сгенерированным серверным компилятором. Поскольку описанный в этом JEP механизм совместного размещения горячего кода динамический, он позволил бы объединить кучи кода клиентского и серверного компиляторов. Это могло бы стать предметом другого JEP.
Теоретически ограничения ITLB можно смягчить с помощью огромных страниц, но Linux (основная целевая ОС) по умолчанию собирает огромные страницы из маленьких синхронно, при отображении огромных страниц (https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html#thp-sysfs), а не в фоновом режиме. Включение прозрачных огромных страниц (transparent huge pages) по умолчанию, если для политики дефрагментации задано значение «defer», вероятно, устранит паузы, возникающие при политике по умолчанию «madvise».
Тестирование
С добавлением стресс-режимов, в которых горячие nmethod многократно перемещаются и уплотняются, существующих тестов, в том числе тестов производительности (стандартных бенчмарков и т. п.), должно быть достаточно. Снижения производительности приложений быть не должно.
Риски и допущения
Надёжность и производительность реализации кэша кода критически важны для надёжности и производительности HotSpot. Чтобы быть уверенными в реализации, необходимы поэтапный подход к реализации и дополнительные режимы стресс-тестирования.
Разделение nmethod может усилить вредную фрагментацию кэша кода, поскольку на каждый nmethod придётся два фрагмента вместо одного. Если две зоны размещения растут с противоположных концов кучи кода к середине, то, пока они не встретятся, будет две соответствующие зоны размещения/фрагментации вместо одной. Смягчающий фактор — каждый из двух фрагментов nmethod меньше целого nmethod, поэтому шансы найти свободные блоки подходящего размера могут быть выше.
Более агрессивное управление кэшем кода связано с конкурентными накладными расходами, но их смягчает неуклонно растущее число аппаратных потоков в современных процессорах. Там, где накладные расходы становятся проблемой, HotSpot может вернуться к существующей политике.
Зависимости
Если будет использоваться механизм сэмплирования профиля методов JFR, возникает зависимость от него.