Оптимизация потребления памяти V8
Потребление памяти является важным аспектом в пространстве компромиссов производительности виртуальной машины JavaScript. За последние несколько месяцев команда V8 проанализировала и значительно уменьшила объем потребляемой памяти на нескольких веб-сайтах, которые были определены как представители современных шаблонов веб-разработки. В этом блоге мы представляем используемые рабочие нагрузки и инструменты анализа, описываем оптимизации памяти в сборщике мусора и показываем, как мы сократили потребление памяти парсером и компиляторами V8.
Тесты производительности
Для профилирования V8 и поиска оптимизаций, которые оказывают существенное влияние на максимальное количество пользователей, крайне важно определить рабочие нагрузки, которые воспроизводимы, значимы и моделируют распространенные сценарии использования JavaScript в реальном мире. Отличным инструментом для этой задачи является Телеметрия, фреймворк тестирования производительности, который запускает запрограммированное взаимодействие с веб-сайтами в Chrome и записывает все ответы сервера, чтобы обеспечить предсказуемое воспроизведение этих взаимодействий в нашей тестовой среде. Мы выбрали набор популярных новостных, социальных и медиаресурсов и определили для них следующие общие действия пользователей:
Рабочая нагрузка для просмотра новостных и социальных веб-сайтов:
- Открыть популярный новостной или социальный сайт, например Hacker News.
- Нажать на первую ссылку.
- Дождаться загрузки нового сайта.
- Прокрутить несколько страниц.
- Нажать кнопку назад.
- Нажать на следующую ссылку на исходном веб-сайте и повторить шаги 3-6 несколько раз.
Рабочая нагрузка для просмотра медиаресурсов:
- Открыть элемент на популярном медиаресурсе, например видео на YouTube.
- Просмотр этого элемента, дождавшись пару секунд.
- Нажать на следующий элемент и повторить шаги 2–3 несколько раз.
После записи рабочего процесса его можно воспроизводить столько раз, сколько необходимо, на тестовой версии Chrome, например каждый раз, когда появляется новая версия V8. Во время воспроизведения используется выборка потребления памяти V8 с фиксированными временными интервалами для получения значимого среднего значения. Тесты производительности можно найти здесь.
Визуализация памяти
Одним из основных вызовов для оптимизации производительности является получение четкого представления о внутреннем состоянии виртуальной машины, чтобы отслеживать прогресс или оценивать возможные компромиссы. В случае оптимизации памяти это означает точное отслеживание потребления памяти V8 во время выполнения. Существует две категории памяти, которые должны быть отслежены: память, выделенная на управляемой куче V8, и память, выделенная на куче C++. Функция Статистика кучи V8 используется разработчиками V8 для глубокого анализа обеих категорий. Когда флаг --trace-gc-object-stats
указан при запуске Chrome (версии 54 или новее) или интерфейса командной строки d8
, V8 выводит статистику, связанную с памятью, в консоль. Мы создали пользовательский инструмент, визуализатор кучи V8, чтобы визуализировать этот вывод. Инструмент показывает временную шкалу как для управляемых, так и для C++ куч. Кроме того, он предоставляет детальную разбивку использования памяти определенных типов данных и гистограммы, основанные на размерах, для каждого из этих типов.
Обычный рабочий процесс во время оптимизации включает выбор типа экземпляра, который занимает значительную часть кучи во временном представлении, как показано на Рисунке 1. После выбора типа экземпляра инструмент показывает распределение использования этого типа. В этом примере мы выбрали внутреннюю структуру данных FixedArray в V8, которая представляет собой нетипизированный контейнер векторного типа, широко используемый в различных частях виртуальной машины. Рисунок 2 показывает типичное распределение FixedArray, где можно увидеть, что большую часть памяти можно отнести к специфическому сценарию использования FixedArray. В этом случае FixedArray используются как хранилище для разреженных JavaScript массивов (то, что мы называем DICTIONARY_ELEMENTS). С этой информацией можно вернуться к исходному коду и либо проверить, соответствует ли это распределение ожидаемому поведению, либо найти возможность для оптимизации. Мы использовали инструмент для выявления неэффективностей с рядом внутренних типов.
На рисунке 3 показано потребление памяти на C++ куче, которая состоит преимущественно из памяти зон (временные области памяти, используемые V8 в течение короткого периода времени; рассмотрено ниже более подробно). Поскольку память зон используется в основном парсером и компиляторами V8, пики соответствуют событиям парсинга и компиляции. Правильное выполнение состоит только из пиков, указывая, что память освобождается, как только она больше не нужна. Напротив, плато (т.е. более долгие периоды времени с высоким потреблением памяти) указывают на возможность оптимизации.
Ранние пользователи также могут опробовать интеграцию с инфраструктурой трассировки Chrome. Для этого необходимо запустить последнюю версию Chrome Canary с параметром --track-gc-object-stats
и записать трассировку, включая категорию v8.gc_stats
. Затем данные появятся в событии V8.GC_Object_Stats
.
Снижение размера JavaScript кучи
Существует врожденный компромисс между пропускной способностью сборки мусора, задержками и потреблением памяти. Например, задержка сборки мусора (которая вызывает видимые пользователю тормоза) может быть снижена использованием большей памяти, чтобы избежать частых вызовов сборки мусора. Для мобильных устройств с низким объемом памяти, то есть устройств с объемом оперативной памяти менее 512 МБ, приоритет задержек и пропускной способности над потреблением памяти может привести к сбоям из-за недостатка памяти и приостановленным вкладкам на Android.
Чтобы лучше сбалансировать правильные компромиссы для этих мобильных устройств с низким объемом памяти, мы ввели специальный режим снижения памяти, который настраивает несколько эвристических подходов сборки мусора для уменьшения использования памяти JavaScript кучи, управляемой сборщиком мусора.
- В конце полной сборки мусора стратегия увеличения кучи V8 определяет, когда произойдет следующая сборка мусора, на основе количества живых объектов с некоторым дополнительным запасом. В режиме снижения памяти V8 использует меньший запас, что приводит к меньшему использованию памяти благодаря более частым сборкам мусора.
- Кроме того, этот расчет рассматривается как жесткое ограничение, заставляя незавершенную работу при инкрементальном маркировании завершаться в основной паузе сборки мусора. Обычно, когда режим снижения памяти не включен, незавершенная работа при инкрементальном маркировании может превышать это ограничение произвольно, чтобы вызвать основную паузу сборки мусора только после завершения маркирования.
- Фрагментация памяти дополнительно уменьшена за счет более агрессивной компактации памяти.
Рисунок 4 изображает некоторые улучшения на устройствах с низким объемом памяти, начиная с Chrome 53. Самое заметное, среднее потребление памяти V8 кучи при тестировании мобильной версии New York Times сократилось примерно на 66%. В целом, мы наблюдали сокращение среднего размера V8 кучи на 50% в этом наборе тестов.
Другая недавно введенная оптимизация не только снижает потребление памяти на устройствах с низким объемом памяти, но и на более мощных мобильных и настольных компьютерах. Снижение размера страницы V8 кучи с 1 МБ до 512 КБ приводит к меньшему объему памяти, когда присутствует мало живых объектов, и уменьшает общую фрагментацию памяти в два раза. Это также позволяет V8 выполнять больше работы по компактации, поскольку меньшие рабочие пакеты позволяют выполнить больше работы параллельно потокам по компактации памяти.
Снижение памяти зон
В дополнение к JavaScript куче V8 использует память вне кучи для внутренних операций виртуальной машины. Наибольший объем памяти выделяется через области памяти, называемые зонами. Зоны — это тип регионального распределителя памяти, который позволяет быстро выделять и массово освобождать память, при этом вся память, выделенная зонами, освобождается одновременно при уничтожении зоны. Зоны используются во всем парсере и компиляторах V8.
Одно из главных улучшений в Chrome 55 связано с уменьшением потребления памяти во время фонового парсинга. Фоновый парсинг позволяет V8 анализировать сценарии во время загрузки страницы. Инструмент визуализации памяти помог нам обнаружить, что фоновый парсер сохранял весь объем памяти зоны долго после того, как код уже был скомпилирован. Немедленно освобождая память зоны после компиляции, мы значительно сократили время жизни зон, что привело к снижению среднего и пикового потребления памяти.
Ещё одно улучшение произошло благодаря более эффективной компоновке полей в узлах абстрактного синтаксического дерева, создаваемых парсером. Ранее мы полагались на компилятор C++, чтобы упаковывать поля вместе, где это возможно. Например, два булевых значения требуют всего два бита и должны находиться внутри одного машинного слова или в неиспользуемой части предыдущего слова. Компилятор C++ не всегда выполняет максимально сжатую упаковку, поэтому мы вручную упаковываем биты. Это не только снижает пиковое использование памяти, но также повышает производительность парсера и компилятора.
На рисунке 5 показаны улучшения пиковой памяти зоны с момента выхода Chrome 54, которые в среднем снижают потребление примерно на 40% на измеряемых веб-сайтах.
В ближайшие месяцы мы продолжим работу над уменьшением занимаемого памяти V8. Мы запланировали больше оптимизаций памяти зоны для парсера и намерены сосредоточиться на устройствах с объёмом памяти от 512 МБ до 1 ГБ.
Обновление: Все описанные выше улучшения позволяют снизить совокупное потребление памяти Chrome 55 на низкопамятных устройствах до 35% по сравнению с Chrome 53. Другие сегменты устройств получают преимущество только от улучшений памяти зоны.