Типизированные массивы в JavaScript

Решил собрать волю в кулак и прочитать-таки документацию про ArrayBuffer, Uint8Array, DataView и всех их родственников. Оказалось, что типизированные массивы это милая и широко-используемая штука по всему фронту JS/HTML5, и я — последний человек на земле, который ими не пользуется.

Вообще-то, обычные массивы тоже отлично работают — размер устанавливается динамически, положить туда можно всё, что угодно, уйма методов для манипуляций есть прямо из коробки. Но в этом есть и недостаток. Первые разработчики WebGL столкнулись с проблемой, что скормить видеокарте JavaScript массив очень проблематично. Ведь нужно пробежаться по всем элементам, привести их к одному типу, скопировать результат в Си-образный массив и уже потом передать указатель видюхе. Как обходной маневр, они создали скриптовый враппер для Си-шных массивов Float. Через какое-то время это породило целый маленький зоопарк типов, которые мы сейчас и рассмотрим.

Точка входа — ArrayBuffer. В принципе, это просто readonly указатель на бинарный буфер в памяти. Читать напрямую из него нельзя. Писать тоже. Зато можно посмотреть размер в байтах и сделать копию.

В этом месте внимательный читатель может задаться вопросом «нафига?». Терпение, мой дорогой товарищ.

Для того, чтобы читать и писать в буфер, нужно создать view, который будет знать, данные какого типа лежат в буфере. Под каждый числовой тип есть свой view. Целых девять штук. Говоря типизированный массив, мы обычно имеем ввиду именно их:

  1. Uint8Array
  2. Int8Array
  3. Uint8ClampedArray
  4. Uint16Array
  5. Int16Array
  6. Uint32Array
  7. Int32Array
  8. Float32Array
  9. Float64Array

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

Что здорово, на один и тот же буфер можно натравить несколько вьюшек, и читать одну и ту же область памяти как, например, массив байт и массив int32 одновременно. Эту фишку можно применить так: есть у нас, например, растр в памяти — последовательность Red, Green, Blue & Alpha компонент цвета каждой точки какой-то картинки. Мы создадим Uint8Array и Uint32Array и сможем читать и писать цвета как по компонентам, так и сразу RGBA точку целиком. Записать int32 будет быстрее, чем четыре int8.

Раз уж пошла речь о растре, Uint8ClampedArray используется canvas для хранения растра текущей картинки. Честно-честно. Если вызвать, например,

canvasContext.getImageData(0, 0, 100, 100).data , то вернется ClampedArray. Он отличается от Uint8Array только тем, что при записи в него значений больше 255, он будет обрезать их до 255. Если меньше нуля — ставить 0. Очень удобно для манипуляций с RGB. А Uint8Array просто отбросит старшие байты записываемого числа, и результат может быть чем угодно в пределах 0-255.

Другой способ работать с буфером — DataView. Он позволяет читать int8-32, uint8-32, float32-64 по заданному смещению. Это удобно, когда мы работаем с ArrayBuffer, в котором смешанные типы данных. Например, когда мы читаем заголовок файла, в котором тип файла может храниться в первом байте, потом идет uint32 контрольной суммы, потом, например, пары широта/долгота GPS координат в float32 до конца файла. Не создавать же типизированный массив для каждого типа поля. А с DataView все получится проще:

Все get/set методы могут взять дополнительный опциональный параметр — little-endian. Он определяет порядок байт в памяти у чисел, которые больше чем один байт. По умолчанию, DataView использует big-endian — от старшего байта к младшему. Процессоры x86, на секунду, используют little-endian. Типизированные массивы используют правила текущей архитектуры, то есть тоже little-endian. То есть можно увидеть такую картину:

Побайтовое сравнение проходит. Мультибайтовое — нет, до тех пор, пока я явно не укажу, что в буфере данные лежат в little-endian формате.

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

Ну и напоследок, кто умеет работать с ArrayBuffer. Во-первых, это XMLHttpRequest (level 2), WebSocket и fetch() — для отправки и получения бинарных сообщений. Canvas для работы с растром, WebGL для буферов (текстур, вершин).  Web Audio API тоже понимает массивы. Самая любопытная и, кажется, недооцененная возможность — передача (Transfer) буфера через postMessage между окном и WebWorker (либо два WebWorker). В этом случае буфер не клонируется, как обычно происходит в postMessage, а просто меняет владельца.  Для предыдущего владельца ссылка становится невалидной. Можно, например, в воркерах собирать тяжелые картинки и по готовности отдавать родительскому окну. Скорость «передачи» данных при этом будет просто огромной.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *