Главная » Хабрахабр » [Перевод] Девять вопросов о работе с памятью в V8

[Перевод] Девять вопросов о работе с памятью в V8

Как известно, JavaScript-движок V8 весьма популярен. Он применяется в браузере Google Chrome, на нём основана платформа Node.js. В материале, подготовленном Мэттом Зейнертом, перевод которого мы публикуем сегодня, приведено девять вопросов, посвящённых особенностям того, как V8 работает с памятью. Каждый вопрос содержит фрагмент кода, который нужно проанализировать и найти ответ, наиболее точно описывающий потребление памяти этим кодом или представленными в нём структурами данных. Ответы на вопросы снабжены комментариями.

image

Разобравшись с этим материалом, вы узнаете о некоторых интересных особенностях того, как V8 обращается с памятью. Возможно, понимание этих особенностей не пригодится вам при поиске проблем с производительностью тех программ, которые вы пишете постоянно, но, полагаем, это поможет вам лучше понять внутренние механизмы движка V8, а значит, может сослужить хорошую службу при анализе какой-нибудь необычной ситуации.

1. Сколько памяти использует каждый элемент массива?

Для ответа на этот вопрос (и на другие подобные вопросы) нужно разделить общую память, потребляемую программой, на длину массива. Здесь мы будем использовать, для указания длины массива, число, представленное переменной MAGIC_ARRAY_LENGTH, равное 1304209. Позже, в комментариях к одному из вопросов, мы остановимся на том, почему здесь используется именно это значение. Пока же отметим, что столь большая длина массива позволяет абстрагироваться от потребления памяти другими частями программ.

Итак, вот код, который вам предлагается проанализировать.

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push(Math.random())
}

Варианты ответа

  1. 1 байт
  2. 4 байта
  3. 8 байт
  4. 16 байт
  5. 24 байта
  6. 35 байт

Выбрав ответ, который вы считаете правильным, разверните текст пояснений и проверьте себя.
Примечание: везде будет «Правильный ответ…» в качестве заголовка сворачиваемого блока.

Правильный ответ…

Правильный ответ на этот вопрос — 8 байт. Дело тут в том, что числа в JavaScript представлены 64-битными значениями с плавающей запятой. В байте 8 бит, в результате каждое число занимает 64/8 = 8 байт.

2. Сколько памяти использует каждый элемент массива?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push(Math.random())
}
a.push("this is a string")

Варианты ответа

  1. 2 байта
  2. 4 байта
  3. 8 байт
  4. 16 байт
  5. 24 байта
  6. 35 байт

Правильный ответ…

В данном случае правильный ответ — 24 байта. В этом примере мы ставим JS-движок в сложное положение. Дело в том, что массив содержит 2 разных типа данных — числа и строки.

В массиве хранится ссылка на строку. Ссылка — это просто указание на то, в какой области памяти хранятся данные (в нашем случае — символы, из которых состоит строка). Адрес в памяти — это число. Системную память можно воспринимать как огромный массив, а адреса в памяти можно считать индексами этого массива.

В результате оказывается, что ссылка на строку — это число, остальные элементы массива — тоже числа. Как их различить? Чем число-ссылка отличается от обычного числа?

Ответ на этот вопрос даёт термин «упакованное значение» («boxed value»). Система упаковывает каждое число в объект и хранит в массиве ссылку на этот объект. Теперь каждый элемент массива может быть ссылкой.

Для того, чтобы сохранить в массиве число, нам нужно поместить в память следующие данные:

  • Ссылку на объект (8 байт)
  • Сам объект, в который упаковано число (16 байт)

Почему объект, представляющий число, занимает 16 байт? Для начала, он должен содержать в себе само число (64-битное значение с плавающей запятой). У каждого JS-объекта есть внутреннее свойство, называемое «скрытым классом» («hidden class»), которое представляет собой ещё одну ссылку.

Почему для хранения ссылки нужно 8 байт? Помните о том, что системная память похожа на массив? Если используется 32-битная система адресации, то с её помощью можно выразить индексы массива вплоть до 2^32. Если вы храните один байт по каждому индексу массива, это значит, что вы можете оперировать 2^32/(1024*1024*1024) = 4 Гб памяти. Так как большинство компьютеров в наши дни имеют больше чем 4 Гб памяти, для работы с ней приходится использовать 64-битные адреса (для хранения адреса требуется 8 байт). Это — довольно упрощённое пояснение происходящего, однако, оно даёт представление о том, как работает система адресации.

3. Сколько памяти использует каждый элемент массива?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push({})
}

Варианты ответа

  1. 8 байт
  2. 16 байт
  3. 32 байта
  4. 64 байта
  5. 128 байт
  6. 156 байт

Правильный ответ…

В данном случае правильным ответом будет 64 байта. Сколько памяти следует выделить движку V8 для хранения пустого объекта? Это — непростой вопрос. В частности, с учётом того, что предполагается, что объект не будет пустым всегда.

Вот список того, что будет хранить V8 для каждого пустого объекта:

  • Ссылка на скрытый класс (8 байт).
  • 4 пустых ячейки для хранения значений будущих свойств объекта (32 байта).
  • Пустая ячейка для хранения ссылки на дополнительный объект, который будет использовать в том случае, если к исходному объекту будет добавлено более 4-х свойств (8 байт).
  • Пустая ячейка для объекта, который хранит значения для индексов числовых свойств (8 байт).

Если вас интересуют подробности о том, как V8 выделяет память для объектов, взгляните на этот материал.

В результате мы приходим к тому, что для одного элемента массива, состоящего из пустых объектов, понадобится 64 байта, в которые входят 56 байт, которые требуются для хранения объекта в памяти, и 8 байт, которые нужны для хранения ссылки на объект в массиве.

4. Сколько памяти использует каждый элемент массива?

var Obj = function(){}
var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push(new Obj())
}

Варианты ответа

  1. 8 байт
  2. 16 байт
  3. 32 байта
  4. 64 байта
  5. 128 байт
  6. 156 байт

Правильный ответ…

Правильным ответом на этот вопрос будет 32 байта. Тут, опять же, в массиве хранятся пустые объекты, но в этот раз мы, для создания объектов, используем функцию-конструктор. V8 может изучить код программы, и понять, что объекты, создаваемые функцией Obj(), ничего не содержат.

Поэтому система может не создавать пустые ячейки для неких ожидаемых свойств, поместив в новый объект всего три 8-байтовых свойства. Вот они:

  • Ссылка на скрытый класс.
  • Ячейка для хранения ссылки на объект с дополнительными свойствами.
  • Ячейка для ссылки на объект, используемый для хранения индексов числовых свойств.

Не стоит забывать и о том, что для хранения ссылок на эти пустые объекты требуется ещё 8 байт. В результате и получается 32 байта.

5. Сколько памяти использует каждый элемент массива?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push("Hello")
}

Варианты ответа

  1. 8 байт
  2. 16 байт
  3. 32 байта
  4. 64 байта
  5. 128 байт
  6. 156 байт

Правильный ответ…

Правильный ответ — 8 байт. Каждый элемент массива должен хранить 64-битную ссылку на текстовое значение, хранящееся в памяти. V8 создаст лишь одну строку, хранящую текст «Hello», а все элементы массива будут ссылаться на неё. Поэтому, если учесть, что у нас имеется достаточно большой массив, размером строки можно пренебречь, и мы придём к тому, что для хранения одного элемента массива V8 понадобится 8 байт.

6. Сколько памяти использует каждый элемент массива?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) { a.push(true)
}

Варианты ответа

  1. 1 байт
  2. 2 байта
  3. 4 байта
  4. 8 байт
  5. 16 байт
  6. 32 байта

Правильный ответ…

Для хранения одного элемента такого массива требуется 8 байт. Значение true сохраняется в массиве в виде ссылки на объект, так же, как это происходит со строками. В результаты нам снова требуется записывать в элементы массива 64-битные адреса. Значения false, undefined и null обрабатываются похожим образом.

7. Каков общий объём памяти, который потребляет эта программа?

var a = []
for (var i=0; i<1024 * 1024; i++) { a.push(Math.random())
}

Варианты ответа

  1. 2 Мб
  2. 4 Мб
  3. 8 Мб
  4. 10 Мб
  5. 16 Мб
  6. 24 Мб

Правильный ответ…

Эта программа потребляет 10 Мб памяти. Тут мы храним в массиве немного больше миллиона чисел, каждое из которых занимает 8 байт. В результате можно предположить, что массив займёт примерно 8 Мб памяти. Однако, на самом деле это не так. Элементы в JavaScript-массивы можно добавлять в любое время, но V8 не будет менять размер массива каждый раз, когда вы добавляете в него новый элемент. Для этого, выделяя память под массив, движок оставляет некоторый объём свободного пространства в конце массива.

В предыдущих примерах мы использовали число, представленное переменной MAGIC_ARRAY_LENGTH. Это число находится на границе «запасной» памяти, которая система выделяет массивам. Значение MAGIC_ARRAY_LENGTH равняется 1304209, в то время как 1024*1024 — это 1048576. Однако и в том и в другом случае объём памяти, используемый массивом, будет одним и тем же.

8. Каков общий объём памяти, который потребляет эта программа?

var a = new Array(1024 * 1024)
for (var i=0; i<1024 * 1024; i++) { a[i] = Math.random()
}

Варианты ответа

  1. 2 Мб
  2. 4 Мб
  3. 8 Мб
  4. 10 Мб
  5. 16 Мб
  6. 24 Мб

Правильный ответ…

В данном случае массиву будет выделено 8 Мб памяти. Так как V8 заранее знает размер массива, система может выделить ему ровно столько памяти, сколько нужно.

9. Каков общий объём памяти, который потребляет эта программа?

var a = new Int16Array(1024 * 1024)
for (var i=0; i<1024 * 1024; i++) { a[i] = 1
}

Варианты ответа

  1. 2 Мб
  2. 4 Мб
  3. 8 Мб
  4. 10 Мб
  5. 16 Мб
  6. 24 Мб

Правильный ответ…

Этой программе понадобится 2 Мб памяти, так как массив содержит лишь 16-битные целые числа, каждое из которых занимает 2 байта. Таких чисел немного больше миллиона, что означает необходимость в 2-х мегабайтах памяти.

Итоги

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

function Holder() {}
var holder = new Holder()
var MAGIC_ARRAY_LENGTH = 1304209
var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH;i++) { a.push(null)
}
holder.a = a

Здесь интересующее нас значение помещено в класс Holder, что упрощает его поиск.

Исследование памяти с помощью инструментов разработчика Chrome

Кстати, автор этого материала говорит, что, проводя эксперименты, он пока не смог до конца понять, как V8 работает в памяти со строками. Если вам удастся это выяснить — уверены, многим будет интересно об этом узнать.

Уважаемые читатели! Какой из представленных в этом материале вопросов показался вам самым сложным?


x

Ещё Hi-Tech Интересное!

[Перевод] Почему важна SRE-документация. Ч.1

Всем добрый вечер! Не успели сентябрьские студенты закончить второй месяц курса «Devops — практики и инструменты», как у нас открывается следующий поток. Интенсивность запусков у нас меняется от месяца к месяцу. Так что мы снова готовы делиться с вами полезными ...

Конспект доклада «Монолит для сотен версий клиентов» (HL2018, Badoo, Владимир Янц)

Продолжаю серию конспектов с HL2018. В проверке этого конспекта мне помогали ребята из Badoo (Владимир Янц vyants и Николай Крапивный), за что им большой спасибо. Надеюсь, это положительно сказалось на качестве донесения идеи доклада. Особенности процесса разработки: Ответственность разработчика не ...