Сжатие указателей в V8
Существует постоянная борьба между памятью и производительностью. Как пользователи, мы хотим, чтобы программы работали быстро и потребляли как можно меньше памяти. К сожалению, обычно улучшение производительности приводит к увеличению расхода памяти (и наоборот).
В 2014 году Chrome перешел с 32-битного процесса на 64-битный процесс. Это обеспечило Chrome лучшую безопасность, стабильность и производительность, но за это пришлось заплатить увеличением потребления памяти, так как теперь каждый указатель занимает восемь байт вместо четырех. Мы приняли вызов сократить это накладные расходы в V8, чтобы вернуть как можно больше потраченных 4 байт.
Прежде чем углубиться в реализацию, нам нужно понять, на каком этапе мы с находимся, чтобы правильно оценить ситуацию. Для измерения потребления памяти и производительности мы используем набор веб-страниц, которые отражают популярные реальные сайты. Данные показывают, что V8 расходует до 60% памяти процесса рендеринга Chrome на настольных компьютерах, в среднем занимая 40%.
Сжатие указателей — это одно из нескольких продолжающихся направлений работы в V8, направленных на снижение потребления памяти. Идея очень проста: вместо хранения 64-битных указателей мы можем хранить 32-битные смещения от некоторого «базового» адреса. Но сколько мы можем выиграть от такого сжатия в V8?
Куча V8 содержит множество элементов, таких как значения с плавающей запятой, символы строк, байт-код интерпретатора и тегированные значения (подробности см. в следующем разделе). Исследование кучи показало, что на реальных веб-сайтах такие тегированные значения занимают около 70% кучи V8!
Давайте подробнее рассмотрим, что такое тегированные значения.
Тегирование значений в V8
JavaScript-значения в V8 представлены как объекты и размещаются в куче V8, независимо от того, являются ли они объектами, массивами, числами или строками. Это позволяет представлять любое значение как указатель на объект.
Многие программы на JavaScript выполняют вычисления с целыми числами, например, инкрементируют индекс в цикле. Чтобы избежать необходимости выделения нового объекта для числа каждый раз при увеличении целого числа, V8 использует хорошо известную технику тегирования указателей для хранения дополнительных или альтернативных данных в указателях кучи V8.
Биты тега выполняют двойную функцию: они либо указывают на сильные/слабые указатели на объекты, расположенные в куче V8, либо представляют собой небольшое целое число. Таким образом, значение целого числа может быть сохранено непосредственно в тегированном значении, без необходимости выделения дополнительного пространства для него.
V8 всегда размещает объекты в куче по выровненным адресам слов, что позволяет использовать 2 (или 3, в зависимости от размера машинного слова) наименее значимых бита для тегирования. На 32-битной архитектуре V8 использует самый младший бит для различения Smis и указателей объектов кучи. Для указателей кучи используется второй младший бит для различения сильных ссылок от слабых:
|----- 32 бита -----| Указатель: |адрес_w1| Smi: |__целое_31_бит0|
где w — бит, используемый для различения сильных указателей от слабых.
Заметьте, что значение Smi может содержать лишь 31-битный полезный заряд, включая бит знака. В случае указателей у нас есть 30 бит, которые могут быть использованы в качестве полезной нагрузки для адреса объекта в куче. Из-за выравнивания слова гранулярность выделения составляет 4 байта, что дает нам 4 ГБ адресуемого пространства.
На 64-битных архитектурах значения V8 выглядят следующим образом:
|----- 32 бита -----|----- 32 бита -----| Указатель: |адресw1| Smi: |целое_32_бит|0000000000000000000|
Вы можете заметить, что в отличие от 32-битных архитектур, на 64-битных архитектурах V8 может использовать 32 бита для полезной нагрузки Smi. Последствия 32-битных Smis для сжатия указателей обсуждаются в следующих разделах.
Сжатые тегированные значения и новая структура кучи
При сжатии указателей нашей целью является каким-то образом разместить оба типа тегированных значений в рамках 32 бит на 64-битных архитектурах. Мы можем поместить указатели в 32 бита, если:
- убедимся, что все объекты V8 выделяются в пределах 4-ГБ диапазона памяти
- представляем указатели как смещения в пределах этого диапазона
Наличие такого жесткого ограничения, к сожалению, неизбежно, но V8 в Chrome уже имеет ограничение в 2 ГБ или 4 ГБ на размер кучи V8 (в зависимости от мощности основного устройства), даже на 64-битных архитектурах. Другие внедренные системы V8, такие как Node.js, могут требовать больших куч. Если мы установим максимальное значение в 4 ГБ, это будет означать, что эти внедренные системы не могут использовать сжатие указателей.
Теперь возникает вопрос, как обновить расположение кучи, чтобы 32-битные указатели однозначно идентифицировали объекты V8.
Тривиальное расположение кучи
Простая схема сжатия заключается в выделении объектов в первых 4 ГБ адресного пространства.
К сожалению, это не подходит для V8, поскольку процесс рендеринга Chrome может потребовать создания нескольких экземпляров V8 в одном процессе рендеринга, например, для Web/Service Workers. В противном случае, при такой схеме все эти экземпляры V8 конкурируют за одно и то же 4-ГБ адресное пространство, и, таким образом, существует ограничение в 4 ГБ памяти, наложенное на все экземпляры V8 вместе.
Расположение кучи, v1
Если мы разместим кучу V8 в непрерывном 4-ГБ регионе адресного пространства где-то еще, то беззнаковое 32-битное смещение от базового значения однозначно идентифицирует указатель.
Если мы также обеспечим, что основание выровнено по 4 ГБ, то верхние 32 бита будут одинаковы для всех указателей:
|----- 32 бита -----|----- 32 бита -----|
Указатель: |________основание_______|______смещение_____w1|
Мы также можем сделать Smis сжимаемыми, ограничив нагрузку Smi до 31 бита и расположив её в нижних 32 битах. По сути, делая их похожими на Smis на 32-битных архитектурах.
|----- 32 бита -----|----- 32 бита -----|
Smi: |sssssssssssssssssss|____int31_значение___0|
где s — это знак нагрузки Smi. Если у нас есть представление с расширением знака, мы можем сжимать и восстанавливать Smis всего одним битовым арифметическим сдвигом 64-битного слова.
Теперь мы видим, что верхняя половина слова как для указателей, так и для Smis полностью определяется нижней половиной слова. Тогда мы можем хранить только последнюю часть в памяти, сокращая объем памяти, необходимой для хранения помеченных значений, вдвое:
|----- 32 бита -----|----- 32 бита -----|
Сжатый указатель: |______смещение_____w1|
Сжатый Smi: |____int31_значение___0|
Учитывая, что основание выровнено по 4 ГБ, сжатие осуществляется просто путем усечения:
uint64_t несжатое_помеченное;
uint32_t сжатое_помеченное = uint32_t(несжатое_помеченное);
Однако, код восстановления немного сложнее. Нужно отличать расширение знака Smi от нулевого расширения указателя, а также учитывать, добавлять ли основание.
uint32_t сжатое_помеченное;
uint64_t несжатое_помеченное;
if (сжатое_помеченное & 1) {
// случай указателя
несжатое_помеченное = основание + uint64_t(сжатое_помеченное);
} else {
// случай Smi
несжатое_помеченное = int64_t(сжатое_помеченное);
}
Попробуем изменить схему сжатия, чтобы упростить код восстановления.
Расположение кучи, v2
Если вместо размещения основания в начале 4 ГБ мы поместим его в середину, мы можем рассматривать сжатое значение как знаковое 32-битное смещение от основания. Обратите внимание, что вся резервация уже не выровнена по 4 ГБ, но основание — да.
В этой новой схеме код сжатия остается неизменным.
Код восстановления, однако, становится проще. Расширение знака теперь одинаково для случаев Smi и указателя, и единственное ветвление — это добавление основания в случае указателя.
int32_t сжатое_помеченное;
// Общий код для обоих случаев: указателя и Smi
int64_t несжатое_помеченное = int64_t(сжатое_помеченное);
if (несжатое_помеченное & 1) {
// случай указателя
несжатое_помеченное += основание;
}
Производительность ветвлений в коде зависит от блока предсказания ветвлений в процессоре. Мы подумали, что если реализуем восстановление без ветвлений, мы сможем добиться лучшей производительности. С помощью небольшого количества битовой магии мы можем написать безветвленную версию кода выше:
int32_t сжатое_помеченное;
// Тот же код для обоих случаев: указателя и Smi
int64_t расширенное_знаковое = int64_t(сжатое_помеченное);
int64_t маска_выбирающая = -(расширенное_знаковое & 1);
// Маска равна 0 в случае Smi или всем единицам в случае указателя
int64_t несжатое_помеченное =
расширенное_знаковое + (основание & маска_выбирающая);
Затем мы решили начать с реализации без ветвлений.
Эволюция производительности
Начальная производительность
Мы замерили производительность на Octane — эталонном тесте пиковой производительности, который мы использовали в прошлом. Хотя мы больше не сосредоточены на улучшении пиковой производительности в повседневной работе, мы также не хотим снижать её, особенно для чего-то столь чувствительного к производительности, как все указатели. Octane продолжает быть хорошим тестовым инструментом для этой задачи.
Этот график показывает результат Octane на архитектуре x64 во время оптимизации и улучшения реализации сжатия указателей. На графике более высокие значения лучше. Красная линия представляет существующую сборку x64 с полных размеров указателей, а зеленая линия — версию со сжатыми указателями.
С первым рабочим вариантом мы наблюдали падение производительности примерно на 35%.
Увеличение (1), +7%
Сначала мы подтвердили нашу гипотезу «безветвленные операции быстрее», сравнивая безветвленное и ветвленное разжатие. Оказалось, что наша гипотеза была неверной, и ветвленная версия была на 7% быстрее на x64. Это была довольно значительная разница!
Давайте взглянем на x64 ассемблерный код.
Разжатие | Безветвленное | Ветвленное |
---|---|---|
Код | ```asm | ```asm \ |
movsxlq r11,[…] | movsxlq r11,[…] \ | |
movl r10,r11 | testb r11,0x1 \ | |
andl r10,0x1 | jz done \ | |
negq r10 | addq r11,r13 \ | |
andq r10,r13 | done: \ | |
addq r11,r10 | ||
``` | ``` | |
Итоги | 20 байт | 13 байт |
^^ | 6 команд выполнено | 3 или 4 команды выполнено |
^^ | без ветвлений | 1 ветвление |
^^ | 1 дополнительный регистр |
r13 здесь — это выделенный регистр, используемый для базового значения. Обратите внимание, что код без ветвлений и больше, и требует больше регистров.
На Arm64 мы наблюдали то же самое — ветвленная версия была явно быстрее на мощных процессорах (хотя размер кода оставался одинаковым для обоих случаев).
Разжатие | Безветвленное | Ветвленное |
---|---|---|
Код | ```asm | ```asm \ |
ldur w6, […] | ldur w6, […] \ | |
sbfx x16, x6, #0, #1 | sxtw x6, w6 \ | |
and x16, x16, x26 | tbz w6, #0, #done \ | |
add x6, x16, w6, sxtw | add x6, x26, x6 \ | |
done: \ | ||
``` | ``` | |
Итоги | 16 байт | 16 байт |
^^ | 4 команды выполнено | 3 или 4 команды выполнено |
^^ | без ветвлений | 1 ветвление |
^^ | 1 дополнительный регистр |
На устройствах начального уровня Arm64 мы наблюдали почти отсутствие разницы в производительности в любом направлении.
Наш вывод: предсказатели ветвлений в современных процессорах очень хороши, и размер кода (особенно длина пути выполнения) больше влияет на производительность.
Увеличение (2), +2%
TurboFan — оптимизирующий компилятор V8, построенный вокруг концепции, называемой «Море узлов». Короче говоря, каждая операция представлена узлом в графе (см. более подробное описание в этой статье). У этих узлов есть различные зависимости, включая как поток данных, так и поток управления.
Существуют две операции, которые имеют решающее значение для сжатия указателей: Загрузка и Сохранение, так как они связывают кучу V8 с остальной частью конвейера. Если бы мы разжимали каждый раз, когда загружали сжатое значение из кучи, и сжимали его перед сохранением, то конвейер мог бы работать так же, как он работал в режиме с полными указателями. Таким образом, мы добавили новые явные операции сжатия и разжатия значений в граф узлов.
Существуют случаи, когда разжатие на самом деле не требуется. Например, если сжатое значение загружено откуда-то только затем, чтобы снова быть сохранено в новом месте.
Чтобы оптимизировать ненужные операции, мы внедрили новый этап «Устранение разжатий» в TurboFan. Его задача — устранять разжатия, за которыми непосредственно следуют сжатия. Поскольку эти узлы могут быть не рядом друг с другом, он также пытается распространять разжатия через граф с надеждой встретить сжатие и устранить их обе. Это дало нам улучшение на 2% в оценке Octane.
Увеличение (3), +2%
Когда мы изучали сгенерированный код, мы заметили, что разжатие значения, которое было только что загружено, производило слишком многословный код.
movl rax, <mem> // загрузить
movlsxlq rax, rax // знаковое расширение
Как только мы исправили это, выполняя знаковое расширение значения, загруженного из памяти, напрямую:
movlsxlq rax, <mem>
так мы достигли ещё 2% улучшения.
Увеличение (4), +11%
Фазы оптимизации TurboFan работают, используя сопоставление с образцом на графе: как только подграф совпадает с определённым шаблоном, он заменяется семантически эквивалентным (но более эффективным) подграфом или инструкцией.
Неудачные попытки найти соответствие не являются явной ошибкой. Наличие явных операций Decompress/Compress в графе привело к тому, что ранее успешные попытки сопоставления с образцом больше не срабатывали, в результате чего оптимизации начали незаметно проваливаться.
Примером "сломавшейся" оптимизации был претернинг аллокации. Как только мы обновили сопоставление шаблонов, чтобы учитывать новые узлы компрессии/декомпрессии, мы получили ещё 11% улучшения.
Дальнейшие улучшения
Увеличение (5), +0.5%
Во время реализации удаления декомпрессии в TurboFan мы узнали много нового. Подход с явным использованием узлов декомпрессии/компрессии имел следующие свойства:
Плюсы:
- Явность таких операций позволила нам оптимизировать ненужные декомпрессии, используя каноническое сопоставление образцов подграфов.
Но в процессе реализации мы обнаружили минусы:
- Комбинаторный взрыв возможных операций преобразования из-за новых внутренних представлений значений стал неуправляемым. Теперь мы могли иметь сжатые указатели, сжатые Smi и сжатые любые (сжатые значения, которые могли быть либо указателем, либо Smi), в дополнение к существующему набору представлений (помеченные Smi, помеченные указатели, помеченные любые, word8, word16, word32, word64, float32, float64, simd128).
- Некоторые существующие оптимизации, основанные на сопоставлении с образцом графа, незаметно перестали выполняться, что привело к регрессиям в некоторых местах. Хотя мы выявили и исправили некоторые из них, сложность TurboFan продолжала расти.
- Аллокатор регистров стал крайне недоволен количеством узлов в графе и довольно часто генерировал плохой код.
- Более крупные графы узлов замедляли фазы оптимизации TurboFan и увеличивали потребление памяти во время компиляции.
Мы решили сделать шаг назад и задуматься о более простом способе поддержки сжатия указателей в TurboFan. Новый подход заключается в отказе от представлений Compressed Pointer / Smi / Any и в том, чтобы сделать все явные узлы сжатия/декомпрессии неявными внутри операций хранения и загрузки, предполагая, что мы всегда декомпрессируем перед загрузкой и компрессируем перед сохранением.
Мы также добавили новую фазу в TurboFan, которая заменит фазу "Удаления декомпрессии". Эта новая фаза будет определять, когда на самом деле не нужно сжимать или декомпрессировать, и обновлять загрузки и сохранения соответствующим образом. Такой подход значительно снизил сложность поддержки сжатия указателей в TurboFan и улучшил качество генерируемого кода.
Новая реализация была столь же эффективной, как и начальная версия, и дала ещё 0.5% улучшения.
Увеличение (6), +2.5%
Мы приблизились к паритету производительности, но разрыв всё ещё оставался. Нам пришлось придумать новые идеи. Одной из них было: что если гарантировать, что любой код, работающий со значениями Smi, никогда не "смотрит" на верхние 32 бита?
Вспомним реализацию декомпрессии:
// Старый способ декомпрессии
int64_t uncompressed_tagged = int64_t(compressed_tagged);
if (uncompressed_tagged & 1) {
// случай указателя
uncompressed_tagged += base;
}
Если верхние 32 бита Smi игнорируются, мы можем считать их неопределёнными. Тогда мы можем избежать специального случая между указателями и Smi и безусловно добавлять базу при декомпрессии, даже для Smi! Мы назвали этот подход "повреждающим Smi".
// Новый способ декомпрессии
int64_t uncompressed_tagged = base + int64_t(compressed_tagged);
Также, так как нас больше не интересует знаковое расширение для Smi, это изменение позволяет нам вернуться к раскладке кучи версии 1. Это раскладка, где база указывает на начало 4ГБ резервирования.
В плане кода декомпрессии это заменяет операцию знакового расширения на нулевое расширение, что столь же дешево. Однако это упрощает вещи на стороне времени выполнения (C++). Например, код резервирования области адресного пространства (см. раздел Некоторые детали реализации).
Вот сравнение ассемблерного кода:
Декомпрессия | С ответвлениями | Нарушение Smi |
---|---|---|
Код | ```asm | ```asm \ |
movsxlq r11,[…] | movl r11,[rax+0x13] \ | |
testb r11,0x1 | addq r11,r13 \ | |
jz done | ||
addq r11,r13 | ||
done: | ||
``` | ``` | |
Резюме | 13 байт | 7 байт |
^^ | выполнено 3 или 4 инструкции | выполнено 2 инструкции |
^^ | 1 ответвление | без ответвлений |
Итак, мы адаптировали все фрагменты кода с использованием Smi в V8 к новой схеме сжатия, что дало нам еще 2,5% улучшения.
Оставшийся разрыв
Оставшийся разрыв в производительности объясняется двумя оптимизациями для 64-битных сборок, которые пришлось отключить из-за фундаментальной несовместимости с сжатием указателей.
Оптимизация 32-битных Smi (7), -1%
Вспомним, как выглядят Smi в полном режиме указателей на 64-битных архитектурах.
|----- 32 бита -----|----- 32 бита -----|
Smi: |____int32_value____|0000000000000000000|
32-битные Smi обладают следующими преимуществами:
- они могут представлять больший диапазон целых чисел без необходимости упаковывать их в числовые объекты; и
- такая форма обеспечивает прямой доступ к 32-битному значению при чтении/записи.
Эта оптимизация невозможна с сжатием указателей, поскольку в 32-битном сжатом указателе нет места из-за наличия бита, который отличает указатели от Smi. Если отключить 32-битные Smi в полной версии указателей на 64-битах, мы увидим 1% регресс в показателе Octane.
Распаковка полей типа Double (8), -3%
Эта оптимизация пытается хранить значения с плавающей точкой непосредственно в полях объекта при выполнении определенных условий. Ее цель — еще больше уменьшить количество выделений числовых объектов, чем это делают сами Smi.
Представьте следующий код JavaScript:
function Point(x, y) {
this.x = x;
this.y = y;
}
const p = new Point(3.1, 5.3);
Вообще говоря, если мы посмотрим, как объект p выглядит в памяти, мы увидим следующее:
Вы можете узнать больше о скрытых классах, свойствах и хранилищах элементов в этой статье.
На 64-битных архитектурах значения типа Double имеют тот же размер, что и указатели. Таким образом, если мы предполагаем, что поля Point всегда содержат числовые значения, мы можем хранить их непосредственно в полях объекта.
Если предположение нарушается для какого-либо поля, скажем, после выполнения этой строки:
const q = new Point(2, 'ab');
тогда числовые значения для свойства y должны храниться упакованными. Кроме того, если где-либо есть оптимизированный код, который опирается на это предположение, он больше не может использоваться и должен быть отброшен (деоптимизирован). Причина такого «обобщения типа поля» — минимизация количества форм объектов, создаваемых одной и той же функцией-конструктором, что, в свою очередь, необходимо для более стабильной производительности.
Применение распаковки полей типа Double дает следующие преимущества:
- обеспечивает прямой доступ к данным с плавающей точкой через указатель на объект, избегая дополнительного обращения через числовой объект; и
- позволяет создавать более компактный и быстрый оптимизированный код для плотных циклов, выполняющих множество обращений к полям типа Double (например, в приложениях для вычислений).
При включенном сжатии указателей значения типа Double просто больше не помещаются в сжатые поля. Тем не менее, в будущем мы можем адаптировать эту оптимизацию для сжатия указателей.
Обратите внимание, что код для вычислений, требующий высокой пропускной способности, можно переписать в оптимизируемом виде даже без этой оптимизации распаковки полей типа Double (в совместимом с сжатием указателей виде), используя хранение данных в Float64 TypedArray или даже Wasm.
Больше улучшений (9), 1%
Наконец, немного точной настройки оптимизации, чтобы устранить декомпрессию в TurboFan, дало еще 1% улучшения производительности.
Некоторые детали реализации
Для упрощения интеграции сжатия указателей в существующий код мы решили распаковывать значения при каждом обращении и сжимать их при каждом сохранении. Таким образом, изменяется только формат хранения помеченных значений, при этом формат выполнения остается неизменным.
Со стороны машинного кода
Чтобы иметь возможность генерировать эффективный код, когда требуется распаковка, базовое значение всегда должно быть доступно. К счастью, в V8 уже был выделенный регистр, всегда указывающий на «таблицу корней», содержащую ссылки на объекты JavaScript и внутренние объекты V8, которые всегда должны быть доступны (например, undefined, null, true, false и многие другие). Этот регистр называется «регистр корня» и используется для генерации более компактного и переиспользуемого кода встроенных функций.
Итак, мы разместили таблицу корней в области резервирования кучи V8, и таким образом регистр корня стал использоваться для обеих целей - как указатель на корни и как базовое значение для распаковки.
Со стороны C++
Среда выполнения V8 получает доступ к объектам в куче V8 через классы C++, предоставляя удобный доступ к данным, хранящимся в куче. Обратите внимание, что объекты V8 скорее похожи на структуры типа POD, чем на объекты C++. Вспомогательные классы «представления» содержат всего одно поле uintptr_t с соответствующим помеченным значением. Поскольку классы представления имеют размер слова, мы можем передавать их значением без дополнительной нагрузки (благодаря современным компиляторам C++).
Вот пример вспомогательного класса:
// Скрытый класс
class Map {
public:
…
inline DescriptorArray instance_descriptors() const;
…
// Фактическое помеченное значение указателя, хранящееся в объекте представления Map.
const uintptr_t ptr_;
};
DescriptorArray Map::instance_descriptors() const {
uintptr_t field_address =
FieldAddress(ptr_, kInstanceDescriptorsOffset);
uintptr_t da = *reinterpret_cast<uintptr_t*>(field_address);
return DescriptorArray(da);
}
Чтобы минимизировать количество изменений, необходимых для первого запуска версии со сжатыми указателями, мы интегрировали вычисление базового значения, необходимого для распаковки, в методы доступа.
inline uintptr_t GetBaseForPointerCompression(uintptr_t address) {
// Округляет адрес вниз до 4 ГБ
const uintptr_t kBaseAlignment = 1 << 32;
return address & -kBaseAlignment;
}
DescriptorArray Map::instance_descriptors() const {
uintptr_t field_address =
FieldAddress(ptr_, kInstanceDescriptorsOffset);
uint32_t compressed_da = *reinterpret_cast<uint32_t*>(field_address);
uintptr_t base = GetBaseForPointerCompression(ptr_);
uintptr_t da = base + compressed_da;
return DescriptorArray(da);
}
Измерения производительности подтвердили, что вычисление базы при каждом обращении ухудшает производительность. Причина в том, что компиляторы C++ не знают, что результат вызова GetBaseForPointerCompression() одинаков для любого адреса из кучи V8, и поэтому компилятор не может объединить вычисления базовых значений. Учитывая, что этот код состоит из нескольких инструкций и 64-битной константы, это приводит к значительному раздутию кода.
Чтобы решить эту проблему, мы переиспользовали указатель экземпляра V8 в качестве базы для распаковки (вспомним данные экземпляра V8 в структуре кучи). Этот указатель обычно доступен в функциях выполнения, поэтому мы упростили код методов доступа, требуя указатель экземпляра V8, и это исправило регрессии:
DescriptorArray Map::instance_descriptors(const Isolate* isolate) const {
uintptr_t field_address =
FieldAddress(ptr_, kInstanceDescriptorsOffset);
uint32_t compressed_da = *reinterpret_cast<uint32_t*>(field_address);
// Округление не требуется, так как указатель Isolate уже является базой.
uintptr_t base = reinterpret_cast<uintptr_t>(isolate);
uintptr_t da = DecompressTagged(base, compressed_value);
return DescriptorArray(da);
}
Результаты
Давайте рассмотрим окончательные результаты Сжатия Указателей! Для этих результатов мы используем те же тесты просмотра, которые мы упоминали в начале данного поста. Напомним, что это истории пользователей, которые мы сочли представительными для использования реальных веб-сайтов.
Мы заметили, что Сжатие Указателей сокращает размер кучи V8 до 43%! В свою очередь, это уменьшает память процесса рендеринга Chrome до 20% на компьютерах.
Еще один важный момент заключается в том, что не каждый веб-сайт улучшает параметры одинаково. Например, объем памяти кучи V8 раньше был больше на Facebook, чем на New York Times, но благодаря Сжатию Указателей ситуация становится противоположной. Эта разница объясняется тем, что на некоторых веб-сайтах больше помеченных значений, чем на других.
Кроме этих улучшений памяти мы также наблюдали улучшения производительности в реальных условиях. На реальных веб-сайтах мы потребляем меньше процессорного времени и времени сборщика мусора!
Заключение
Путь сюда не был усыпан розами, но это было того стоит. 300+ коммитов спустя, V8 с компрессией указателей использует столько же памяти, как если бы мы запускали 32-битное приложение, при этом имея производительность 64-битного.
Мы всегда стремимся к улучшению и в нашем плане имеются следующие связанные задачи:
- Улучшить качество генерируемого ассемблерного кода. Мы знаем, что в некоторых случаях мы можем генерировать меньше кода, что должно улучшить производительность.
- Решить связанные проблемы ухудшения производительности, включая механизм, позволяющий снова распаковывать поля типа double удобным для компрессии указателей способом.
- Исследовать идею поддержки более крупных куч в диапазоне от 8 до 16 ГБ.