Введение в ArrayBuffers и SharedArrayBuffers в картинках

Перевод второй статьи из серии статей от Lin Clark:

  1. Ускоренный курс по управлению памятью
  2. Введение в ArrayBuffers и SharedArrayBuffers в картинках
  3. Как избежать режима конкуренции (race conditions) в SharedArrayBuffers с помощью Atomics

В последней статье я объяснила как ЯП с автоматическим управлением памятью, JavaScript например, работают с памятью. Также я объяснила как работают ЯП с ручным управлением памятью, С например.

Почему же это так важно когда мы говорим о ArrayBuffers и SharedArrayBuffers?

Все потому, что ArrayBuffers дают вам возможность обрабатывать некоторые ваши данные вручную, не смотря на то, что вы пишете на JavaScript, который является ЯП с автоматическим управлением памятью.

Почему же вас вообще должно волновать наличие такой возможности?

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

Вот еще нюанс. Когда вы хотите объявить переменную в JS, JS движок должен угадать тип переменной (JS не строго типизированный ЯП) и прикинуть в каком виде ее хранить в памяти. И именно потому, что нужно угадать тип переменной и не понятно сколько места для нее нужно выделить, JS движок зарезервирует «чуть больше, с запасом». В зависимости от типа переменной выделенное количество памяти может быть больше в 2–8 раз, чем нужно в действительности.

В дополнение к этому, некоторые паттерны/техники создания и использования JS объектов при разработке делают сборку мусора весьма затруднительной (прим. пер.: см. описание проблемы двумя параграфами выше). Но, благодаря инструментарию управления памятью вручную, вы можете сами выбирать стратегию выделения и освобождения памяти, которая подойдет именно для вашей ситуации.

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

Но для тех случаев, когда вам нужно работать на низком уровне, чтобы сделать ваш код настолько быстрым, насколько это возможно, вам сильно пригодятся ArrayBuffers и SharedArrayBuffers.

Так как же работает ArrayBuffer?

В основном так же, как и любой другой JavaScript массив. За исключением того, что когда вы используете ArrayBuffer, вы не можете поместить в него какой-либо JavaScript тип, например объект или строку. Единственное, что вы можете поместить в ArrayBuffer — это байты (которые можно представить в виде чисел).

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

Для того, чтобы предоставить контекст, который фактически разобьет эту последовательность нулей и единиц на ячейки мы должны обернуть ArrayBuffer в так называемое представление (ориг. view). Эти представления для данных могут быть добавлены с помощью типизированных массивов (ориг. typed arrays), и существует множество видов типизированных массивов для работы с ArrayBuffer.

Например, вы можете использовать Int8 типизированный массив, который разобьет данные в ArrayBuffer на 8-битные кусочки.

Или у вас может быть беззнаковый Int16 массив, разбивающий ArrayBuffer на 16-битные кусочки, а также обрабатывающий значения так, как если бы это были целые числа без знака.

Вы даже можете иметь несколько представлений для одного базового буфера. Разные представления будут давать вам разные результаты при одних и тех же операциях.

Например, если мы хотим получить 0 и 1 элементы с помощью Int8 представления для конкретного ArrayBuffer, мы получим значения, которые отличаются от значения, взятого из того же ArrayBuffer но с помощью Int16 представления. Хотя, по факту, это один и тот же набор битов.

Таким образом, ArrayBuffer в основном работает как необработанная/сырая (ориг. raw) память. Он эмулирует вид прямого доступа к памяти, который возможен на языке, подобном C.

«Почему бы нам просто не дать программистам прямой доступ к памяти вместо добавления этого слоя абстракции?» — спросите вы. Прямой доступ к памяти создает некоторые дыры в безопасности. Об этом я расскажу в следующей статье.

Итак, что же такое SharedArrayBuffer?

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

Возможно вы хотите исполнять ваш код параллельно, чтобы он работал быстрее или чтобы он быстрее реагировал на пользовательские события. Для этого вам нужно разделить работу.

В типичном приложении о всей работе заботится одна сущность — основной поток (ориг. main thread). Я упоминала об этом чуть раньше (прим. пер.: видимо в других статьях). Основной поток — это как full-stack разработчик. Он отвечает и за JavaScript, и за DOM, и за разметку.

Все, что вы можете сделать для уменьшения нагрузки на основной поток, помогает увеличить быстродействие. И, в определенных обстоятельствах, ArrayBuffer может уменьшить количество задач, которые должен выполнять основной поток.

Но бывают случаи, когда уменьшения нагрузки на основной поток не достаточно. Иногда вам нужно вызывать подкрепление (прим. пер.: аналогия с военными подкреплениями)… вам нужно разделить работу.

В большинстве ЯП способ, с помощью которого вы можете разделить работу, представляет собой использование так называемых потоков (ориг. thread). Это похоже на то, что вместо одного разработчика, над проектом начинают работать еще несколько. Если у вас есть задачи, которые не зависят друг от друга, вы можете назначить их разным потокам. Таким образом, потоки могут работать каждый над своей задачей одновременно.

В JavaScript вы можете реализовать это с помощью так называемых web worker-ов. Эти web worker-ы немного отличаются от потоков, используемых в других ЯП. По умолчанию они не делят память между собой. Т.е. каждый из них использует разные участки памяти.

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

postMessage принимает любой объект, который вы в нее передали, сериализует его, шлет в другой web worker, где объект десериализуется и записывается в память.

Это довольно медленный процесс.

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

Но после такого переноса первый web worker теряет доступ к перенесенному участку памяти.

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

Именно этого можно добиться, используя SharedArrayBuffer.

С SharedArrayBuffer оба web worker-а, оба потока, могут писать и читать данные с одного и того же участка памяти.

Это значит, что при коммуникации между ними не возникает накладных расходов в виде сериализации, копирования и десериализации данных как в случае с postMessage.

Стоить отметить, что есть и некоторые риски в том, что несколько web worker-ов в один и тот же момент времени имеют доступ к одному и тому же участку памяти. Это может повлечь за собой появление так называемого режима конкуренции (ориг. race conditions).

Я расскажу больше об этом в следующей статье.

Каков текущий статус SharedArrayBuffers?

SharedArrayBuffers будут имплементированы во всех мажорных браузерах… скоро…

Они уже доступны в Safari (Safari 10.1). Firefox и Chrome обещают поддержку после релизов в июле/августе (2017). Edge обзаведется поддержкой после осеннего обновления Windows.

Даже после того, как поддержка появится во всех мажорных браузерах, мы (прим. пер.: не совсем понятно кто конкретно) не рекомендуем разработчикам приложений использовать SharedArrayBuffers напрямую. Им следует использовать более высокоуровневые абстракции для работы с памятью. Мы ожидаем, что разработчики JavaScript библиотек будут предоставлять эти самые высокоуровневые абстракции, используя SharedArrayBuffers в коде своих библиотек.

В дополнение к вышесказанному, как только поддержка SharedArrayBuffers появится на уровне платформы, WebAssembly сможет использовать их для реализации поддержки потоков. Т.е. код на поддерживающих потоки ЯП, будет преобразован в JavaScript код, использующий SharedArrayBuffers. Как только это будет реализовано вы сможете использовать абстракции для работы с конкурентностью (ориг. the concurrency abstractions) из таких языков как Rust, который позиционирует беспроблемную конкуренцию одной из своих основных целей.

В следующей статье мы рассмотрим один из инструментов (Atomics), который могут использовать разработчики JavaScript библиотек для предоставления абстракции, избавляющей от режимов конкуренции (ориг. race conditions).

О Lin Clark

Лин работает инженером в команде Mozilla Developer Relations. Она занимается JavaScript, WebAssembly, Rust и Servo, а также рисует комиксы про то, как работает наш код.

More articles by Lin Clark…


Originally published at muntianblog.wordpress.com on July 12, 2017.