openjdk.ruOpenJDK на русском

JEP 157: G1 GC: NUMA-Aware Allocation

G1 GC: выделение памяти с учётом NUMA

АвторY. Srinivas Ramakrishna
ОтветственныйJesper Wilhelmsson
ТипFeature
ОбластьImplementation
СтатусClosed / Withdrawn
Компонентhotspot / gc
Обсуждениеhotspot dash gc dash dev at openjdk dot java dot net
ТрудоёмкостьS
ДлительностьM
DuplicatesJEP 345: NUMA-Aware Memory Allocation for G1
РецензентыIgor Veresov, Jesper Wilhelmsson, Jon Masamitsu, Paul Hohensee, Tony Printezis
ОдобренMikael Vidstedt
Создан2011/07/28 20:00
Обновлён2018/09/18 19:31
Задача8046147

Аннотация

Доработать G1, чтобы повысить производительность выделения памяти в системах с памятью NUMA.

Что не является целью

Распространить учёт NUMA на какие-либо ОС, кроме Linux и Solaris, в которых есть подходящие интерфейсы NUMA.

Мотивация

Современные многосокетные машины всё чаще имеют архитектуру NUMA: не вся память равноудалена от каждого сокета или ядра. Более традиционные SMP-системы с обычной архитектурой «dance hall» встречаются всё реже, разве что, пожалуй, в самом верхнем сегменте. Возможно, причина в стоимости и сложности масштабирования таких архитектур и в вытекающих отсюда ограничениях их межсоединений по задержке и пропускной способности. Большинство современных ОС, начиная с Solaris около десяти лет назад, теперь предоставляют интерфейсы, через которые можно запросить топологию памяти платформы и предпочтительно отображать физическую память из определённой группы локальности. ParallelScavengeHeap в HotSpot уже много лет учитывает NUMA. Это помогло масштабировать производительность конфигураций, в которых одна JVM работает на нескольких сокетах и видит платформу NUMA. Некоторые другие сборщики HotSpot, прежде всего конкурентные, этой возможности не получили и не могут воспользоваться таким вертикальным масштабированием на многосокетных NUMA-системах. Крупные корпоративные приложения работают с большими кучами и нуждаются в мощности нескольких сокетов, но при этом хотят сохранить удобство управления, которое даёт работа в одной JVM. Поэтому клиенты, использующие наши конкурентные сборщики, будут всё чаще упираться в это узкое место масштабирования.

Цель этого JEP — распространить учёт NUMA на кучу, которой управляет сборщик мусора G1.

Описание

Куча G1 организована как набор регионов фиксированного размера, взятых из интервала виртуального адресного пространства, который в текущей реализации оказывается сплошным. Поколения, или отдельные логические пространства (например, Eden, Survivor и Old), затем формируются как динамические непересекающиеся подмножества этого набора регионов. Обычно регион — это набор физических страниц. Однако при использовании очень больших страниц (скажем, суперстраниц 256M на SPARC) одну физическую страницу могут составлять несколько регионов.

Чтобы выделение памяти в G1 учитывало NUMA, мы сначала сосредоточимся на так называемых регионах Eden. Регионы Survivor можно будет рассмотреть на втором этапе улучшений, но они не входят в рамки этого JEP. Если говорить совсем обобщённо, мы хотим, чтобы регионы Eden брались из набора физических страниц, выделенных в определённых группах локальности (далее — «lgrps»). Идея аналогична пространствам NUMA, которые использует ParallelScavengeHeap. За неимением лучшего выражения назовём их «пулами регионов для каждой lgrp».

Мы представляем жизненный цикл региона Eden примерно так:

  • Каждый регион начинается как нетронутый регион без выделенных физических страниц.

  • У регионов Eden базовые страницы выделены в определённых группах локальности.

  • Изначально регион нетронут и не связан ни с одной конкретной группой локальности.

  • Каждый поток при запуске запрашивает и запоминает свою домашнюю lgrp (далее для краткости — «lgrp потока»).

  • Когда поток, у которого lgrp равна L, запрашивает TLAB, мы ищем в пуле регионов для L. Если в L есть текущий регион выделения, он используется, чтобы удовлетворить запрос на выделение TLAB. Если текущий регион выделения равен NULL или свободного места в нём слишком мало для запроса TLAB, то из пула регионов для L выделяется новый регион. Он становится текущим регионом выделения, который обслужит этот и последующие запросы TLAB. Этот регион уже использовался ранее, и ему уже выделены страницы из lgrp L. Если пул регионов для L пуст, мы проверяем, есть ли свободный регион Eden в глобальном пуле, и этот регион затем назначается пулу L. В этот момент регион нетронут и страниц ему не выделено (или последний раз он был освобождён через madvise). Подходящий API lgrp (предписывающий или описательный) используется, чтобы физические страницы для этого региона выделялись в локальной lgrp L.

  • Если в глобальном пуле (нетронутых) регионов Eden нет доступных регионов, а увеличить Eden нельзя (по соображениям политики или по другим причинам), выполняется сборка молодого поколения (scavenge). Альтернатива — забрать у другой lgrp регион, уже привязанный к ней, но не занятый, и перенести его в эту lgrp. Однако предложенная выше политика следует политике, реализованной в PS, где такой перенос по требованию оказался менее эффективным, чем адаптивный перенос после сборки молодого поколения (см. ниже).

  • При каждой сборке молодого поколения оценивается заполненность пулов каждой lgrp. По взвешенному среднему за среднесрочный период или в скользящем окне определяется, есть ли неиспользуемые или частично используемые регионы, которые нужно освободить через madvise, чтобы адаптивно изменить размер пулов lgrp.

  • Humongous-регионы естественным образом исключаются из этой политики выделения, поскольку такие регионы в любом случае не считаются частью Eden, так что ничего особенного для них делать не потребуется. (Разумной политикой для таких регионов может быть чередование страниц или их случайное равномерное выделение по всем lgrp, чтобы оптимизировать производительность в худшем случае, если предполагать равномерный случайный доступ из каждой lgrp.)

ParallelScavengeHeap выделяет страницы из пространства survivor по кругу (round-robin). Как сказано выше, привязка регионов Survivor к NUMA не является целью этого JEP.

При использовании больших страниц, когда несколько регионов отображаются на одну и ту же физическую страницу, всё несколько усложняется. Пока мы обойдём это, отключая оптимизации NUMA, как только размер страницы превышает небольшое кратное размера региона (скажем, 4), а более общим случаем займёмся на отдельном последующем этапе. Когда размер страницы ниже этого порога, мы будем выделять непрерывные наборы регионов и привязывать их к пулам Eden для каждой lgrp. Автор этого текста недостаточно знаком с текущей политикой выделения регионов, но полагает, что для этого, скорее всего, потребуются небольшие изменения в существующей политике выделения регионов в G1, чтобы можно было выделять сразу набор регионов.

Ключ командной строки -XX:+UseNUMA command должен включать эту возможность для G1, если также используется -XX:+UseG1GC. Если окажется, что этот параметр хорошо работает для большого класса программ, мы можем включить его по умолчанию на платформах NUMA (как, по-моему, сегодня сделано для ParallelScavenge). Другие параметры, связанные с адаптацией к NUMA и её возможностями, должны поддерживаться так же, как для кучи ParallelScavenge. По возможности следует избегать параметров NUMA, специфичных для отдельного сборщика.

Тестирование

Обычное тестирование (с -XX:+UseNUMA, где это уместно) должно выявить любые проблемы корректности. Этот JEP предполагает использование оборудования NUMA для тестирования. Будет проведено целевое тестирование производительности на различных бенчмарках и приложениях, на различных платформах с NUMA и без неё.

Риски и допущения

Как и в случае сборщика ParallelScavenge, реализация здесь исходит из допущения, что к большинству короткоживущих объектов чаще всего обращается поток, который их выделил. Это определённо верно для большинства короткоживущих объектов в большинстве объектно-ориентированных программ, как уже показал нам опыт с ParallelScavenge. Однако есть небольшой класс программ, для которых это допущение не вполне выполняется. Выигрыш также зависит от соотношения между степенью неоднородности памяти (NUMA) нижележащей системы и накладными расходами на перенос страниц в таких системах, особенно при частых переносах потоков под высокой нагрузкой. Наконец, возможно, существуют платформы, на которых подходящие интерфейсы lgrp либо не являются общедоступными или вообще отсутствуют, либо не были реализованы по другим причинам.

Есть некоторый риск, что закрепление регионов за пулами определённых lgrp несколько снизит гибкость при перемещении регионов между различными логическими пространствами, но мы не считаем это серьёзным препятствием.

Несколько серьёзнее то, что закрепление регионов за пулами lgrp вызовет некоторую внутреннюю фрагментацию в этих пулах, что не так уж отличается от ситуации с ParallelScavengeHeap. Это известная проблема. Поскольку единица выделения по lgrp в ParallelScavengeHeap — страница, а в G1 — регион, который может состоять из нескольких (меньших) страниц, мы, как правило, не ожидаем, что реализация в G1 будет работать сколько-нибудь лучше, чем в ParallelScavengeHeap.