Улучшенное кэширование кода
V8 использует кэширование кода для кэширования сгенерированного кода часто используемых скриптов. Начиная с Chrome 66, мы кэшируем больше кода, создавая кэш после выполнения верхнего уровня. Это приводит к сокращению времени анализа и компиляции на 20–40% при первоначальной загрузке.
Фон
V8 использует два вида кэширования кода для повторного использования сгенерированного кода в будущем. Первый вид — это кэш в памяти, который доступен внутри каждого экземпляра V8. Код, созданный после первоначальной компиляции, сохраняется в этот кэш с ключом, основанным на строке исходного кода. Этот кэш доступен для повторного использования в рамках одного экземпляра V8. Второй вид кэширования кода сериализует сгенерированный код и сохраняет его на диск для будущего использования. Этот кэш не зависит от конкретного экземпляра V8 и может использоваться в разных экземплярах V8. В этом блоге мы сосредоточимся на втором виде кэширования в контексте использования в Chrome. (Другие встроенные движки также используют этот вид кэширования кода; он не ограничивается Chrome. Однако в этом блоге речь идет только о Chrome.)
Chrome сохраняет сериализованный сгенерированный код в дисковом кеше и ключует его URL-адресом ресурса скрипта. При загрузке скрипта Chrome проверяет дисковый кэш. Если скрипт уже находится в кэше, Chrome передает сериализованные данные V8 как часть запроса на компиляцию. V8 десериализует эти данные вместо анализа и компиляции скрипта. Также выполняются дополнительные проверки для обеспечения того, что код все еще можно использовать (например: несоответствие версии делает данные кэша непригодными для использования).
Данные реального мира показывают, что процент попаданий в кэш кода (для скриптов, которые можно кэшировать) высок (~86%). Хотя процент попаданий в кэш для этих скриптов высок, объем кода, который мы кэшируем на каждый скрипт, не очень большой. Наш анализ показал, что увеличение объема кэшируемого кода позволяет сократить время, затрачиваемое на разбор и компиляцию JavaScript-кода, примерно на 40%.
Увеличение объема кэшируемого кода
В предыдущем подходе кэширование кода было связано с запросами на компиляцию скрипта.
Встроенные движки могли запросить, чтобы V8 сериализовал код, который он сгенерировал при компиляции нового исходного файла JavaScript на верхнем уровне. V8 возвращал сериализованный код после компиляции скрипта. Когда Chrome снова запрашивает тот же скрипт, V8 извлекает сериализованный код из кэша и десериализует его. V8 полностью избегает повторной компиляции функций, которые уже находятся в кэше. Эти ситуации показаны на следующем рисунке:
V8 компилирует только те функции, которые предполагается немедленно выполнить (IIFEs) при начальной верхнеуровневой компиляции, и помечает другие функции для ленивой компиляции. Это помогает сократить время загрузки страницы, избегая компиляции функций, которые не требуются, однако это означает, что сериализованные данные содержат только код для функций, которые компилируются активно.
До Chrome 59 нам нужно было создавать кэш кода до начала выполнения любого кода. Ранее базовый компилятор V8 (Full-codegen) генерировал специализированный код для контекста выполнения. Full-codegen использовал замену кода для ускорения операций, специфичных для контекста выполнения. Такой код нельзя легко сериализовать, удалив данные, специфичные для контекста, чтобы использовать их в других контекстах.
С выпуском Ignition в Chrome 59 это ограничение больше не требуется. Ignition использует инлайн-кэши, основанные на данных для ускорения операций в текущем контексте выполнения. Данные, зависящие от контекста, хранятся в векторах обратной связи и отделены от сгенерированного кода. Это открыло возможность создания кэша кода даже после выполнения скрипта. По мере выполнения скрипта больше функций (которые были помечены для ленивой компиляции) компилируются, позволяя нам кэшировать больше кода.
V8 предоставляет новый API, ScriptCompiler::CreateCodeCache
, для запроса кеша кода независимо от запросов компиляции. Запросы кеша кода вместе с запросами компиляции устарели и не будут работать в V8 начиная с версии 6.6. Начиная с версии 66, Chrome использует этот API для запроса кеша кода после выполнения верхнего уровня. На следующем рисунке показан новый сценарий запроса кеша кода. Кеш кода запрашивается после выполнения верхнего уровня и, следовательно, содержит код для функций, которые были скомпилированы позже, во время выполнения скрипта. Во время последующих запусков (показанных как «горячие» запуски на следующем рисунке) это позволяет избежать компиляции функций во время выполнения верхнего уровня.
Результаты
Производительность этой функции измеряется с использованием наших внутренних тестов приближенных к реальному миру. На следующем графике показано снижение времени разбора и компиляции по сравнению с предыдущей схемой кеширования. Снижение составляет около 20–40% как времени разбора, так и времени компиляции на большинстве страниц.
Данные из реального мира показывают схожие результаты со снижением времени, затраченного на компиляцию JavaScript-кода, на 20–40% как на настольных компьютерах, так и на мобильных устройствах. На Android эта оптимизация также приводит к снижению основных метрик загрузки страницы на 1–2%, например времени, необходимого для интерактивности веб-страницы. Мы также отслеживали использование памяти и диска Chrome и не заметили никаких заметных регрессий.