javascript

ArrayBuffer и SharedArrayBuffer в JavaScript, часть 1: краткий курс по управлению памятью

  • четверг, 22 июня 2017 г. в 03:14:46
https://habrahabr.ru/company/ruvds/blog/331344/
  • JavaScript
  • Блог компании RUVDS.com


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

JavaScript — это как раз тот случай, когда управление памятью выполняется в автоматическом режиме, однако, появление ArrayBuffer и SharedArrayBuffer меняет ситуацию.



Для того, чтобы понять, что именно приносят в JS-разработку ArrayBuffer и SharedArrayBuffer, почему эти объекты появились в языке, предлагаем начать с самого начала, а именно — с разговора об управлении памятью.

Память и работа с ней


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


Шкафчики, в один из которых некто собирается что-то положить

Около каждого из ящиков имеется номер, который, в применении к оперативной памяти, представляет собой адрес. Этот адрес используется в том случае, если кому-то надо сообщить о том месте, где мы для него что-то оставили.

Все ящики имеют одинаковый размер и могут хранить определённый объём информации. Сколько именно — зависит от конкретной компьютерной архитектуры. Этот размер называется размером слова памяти. Обычно это что-то вроде 32-х или 64-х бит. Для простоты изложения мы будем использовать 8-ми битные слова.


Блок из восьми ячеек

Если в одну из таких 8-ми битных ячеек надо поместить, например, число 2, сделать это очень просто, так как числа легко переводятся в двоичную систему счисления.


Десятичное число 2 преобразовано в двоичное 00000010, которое размещено в ячейке памяти

Что, если в ячейку надо поместить нечто, не являющееся числом? Например — латинскую букву H?
В таком случае нам надо отыскать способ представить букву в виде числа. Для того, чтобы это сделать, букву надо как-то закодировать, например, воспользоваться кодировкой UTF-8. Далее, нужен способ преобразования буквы в число, что-то вроде кодирующего устройства в виде кольца. Букву, после преобразования, тоже можно сохранить в 8-ми битной ячейке.


Буква H преобразована, с помощью кодировщика, в десятичное число 72, которое, после преобразования в двоичную систему счисления, размещено в ячейке памяти

Когда букву надо достать из ячейки, нам понадобится пропустить число, представляющее её, через декодер, чтобы, вместо числа, у нас снова была буква H.

Автоматическое управление памятью


Программируя на JavaScript, на самом деле, можно не думать о том, что происходит с памятью. Данная часть системы абстрагирована от разработчика. Это означает, что напрямую с памятью он не работает.

Вместо этого JS-движок действует в роли посредника. Он отвечает за управление памятью.


Набор ячеек памяти за ограждением, доступ к которым контролирует JS-движок

Итак, предположим, некий JS-код, вроде библиотеки React, хочет создать переменную.


React просит JS-движок создать строковую переменную

Получив запрос на создание переменной и присваивание ей некоего значения, JS-движок пропускает это значение через кодировщик для получения его двоичного представления.


JS-движок преобразует строковую переменную в её двоичное представление

Затем движок ищет место в памяти, куда он может поместить полученное двоичное представление. Это называется выделением памяти.


JS-движок ищет свободное место для двоичных данных

После этого движок наблюдает за тем, доступна ли переменная из какого-либо места программы. Если в программе переменная уже не доступна, неиспользуемая память должна быть возвращена для дальнейшего использования, после чего JS-движок сможет разместить в освободившейся памяти новые данные.


Сборщик мусора очищает память

Процесс наблюдения за переменными — строками, объектами, и чем угодно другим, хранящимся в памяти, и очистки памяти после того, как к хранящимся в ней данным нельзя получить доступ из программы, называется сборкой мусора.

Языки вроде JavaScript, в которых из программ нельзя напрямую работать с памятью, называются языками с автоматическим управлением памятью.

Автоматическое управление памятью может упростить жизнь разработчикам. Но оно ведёт к дополнительной нагрузке на систему. Иногда эта нагрузка оказывает непредсказуемое влияние на производительность.

Ручное управление памятью


Существуют разные языки, поддерживающие ручное управление памятью. Например, представим себе, как работал бы React, будь он написан на C (на самом деле, сейчас подобное, благодаря WebAssembly, уже возможно).

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


WebAssembly-версия React работает с памятью напрямую

Компилируя то, что написано на C или на других языках, в WebAssembly, используемый инструмент,  добавит к коду программы некоторые вспомогательные элементы. Например, это может быть подсистема, которая занимается кодированием и декодированием байтов. Этот вспомогательный код называется средой выполнения приложения. В JavaScript такую роль играет JS-движок.


Средство кодирования символов добавлено в .wasm-файл

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

Это не означает, что задача по управлению памятью всецело возлагается на программиста. Даже в языках с ручным управлением памятью среда выполнения обычно оказывает некоторую помощь в этом деле. Например, в C она наблюдает за тем, какие адреса памяти доступны, храня их в списке свободной памяти.


Список свободной памяти отражает текущее состояние системы

Тут можно использовать функцию malloc (её название — сокращение от memory allocation, то есть — выделение памяти) для того, чтобы попросить среду выполнения найти адреса блоков памяти, которые подходят для хранения данных. После выделения памяти соответствующие адреса будут убраны из списка свободной памяти. Когда сохранённые в памяти данные больше не нужны, приходит время воспользоваться командой free для того, чтобы освободить память. Затем адреса освобождённых блоков памяти снова попадут в список свободной памяти.

Программисту нужно самостоятельно решать — когда именно вызывать вышеупомянутые функции. Именно поэтому то, о чём мы говорим, называется «ручным управлением памятью».

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

Итоги


Именно для того, чтобы избежать человеческих ошибок, множество современных языков используют автоматическое управление памятью. Однако, при таком подходе страдают производительность системы и эффективность использования памяти. Подробнее об этом — в следующий раз.

Уважаемые читатели! Как по-вашему, что изменилось бы в JavaScript-разработке, если бы язык поддерживал ручное управление памятью без каких-либо вспомогательных механизмов вроде сборщика мусора?