🧊

JavaScriptのTypedArrayについて

調べたのでメモ。

TypedArray

TypedArray - JavaScript | MDN

いわゆる型付き配列。
通常の配列と違って型が固定できる分、内部的に最適化がしやすいとか諸々で住み分けられてる。

そういう意味で一般的なフロントエンドのJavaScriptコードで出てくることはそうそうない。

たとえばWebAudioとか、CanvasとかWebGLとか、いわゆるバイナリに近い処理をブラウザでやる場合に必要になるAPIたち。
Node.jsだと、`Buffer`っていうそれ用のクラスがあったりする。

種類

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BigUint64Array

というように、並ぶアイテムの型によって名前が決まる。

Uint8の場合、符号なし8bit(1byte)なので、0から255までしか扱えない。
格納したいアイテムによって、使う種類を選ぶもの。

`Uint8ClampedArray`は特別なやつで、`255`より大きい値をいれると`255`に、`0`未満は`0`にしてくれるやつ。

ArrayBuffer

で、この`TypedArray`たちは、内部的なデータ構造としてそれぞれ`ArrayBuffer`を持つ。

というかデータの実態はこの`ArrayBuffer`であり、その見てくれ表現として各`TypedArray`がいる感じ。

const f32 = new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]);
f32.length; // 8

f32.buffer; // ArrayBuffer(32)
f32.byteLength; // 32

で、共通した`ArrayBuffer`を持つということで、各`TypedArray`は実は相互変換ができる。
つまり`Uint8Array([0, 1])`と`new Uint16Array([256])`は同等の`ArrayBuffer`を持つということ。

const uint8 = new Uint8Array([0, 1]);
const uint16 = new Uint16Array(uint8.buffer);
new Uint8Array(uint16.buffer); // Uint8Array([0, 1]);

符号とかレンジとかはよしなになるっぽい。

// Uint8Array(7) [0, 6, 255, 0, 1, 255, 2]
new Uint8Array(
  new Int8Array([-256, -250, -1, 0, 1, 255, 258]).buffer
);

ただし要素数が足りない場合はエラーになる。

// Uncaught RangeError: byte length of Int16Array should be a multiple of 2
new Int16Array(new Int8Array([0, 1, 2]).buffer);

その裏側が知りたい

`Uint8Array([0, 1])`が`Int16Array([256])`と相互変換できるのは良いとして、この`256`ってどこから来た?って思ったので調べた。

  • `256`を2進数に
    • `(256).toString(2)`は、`100000000`
  • `100000000`は、バイト列にすると`00000001|00000000`
  • それが`00000000|00000001`となって(順序が逆になる)
  • つまり`0`と`1`に

で、このバイト列がメモリ上で、CPU上で、どう扱われてるかにトリックがある。

この例でしれっと順序が逆になるって書いたのは、リトルエンディアン(LE)という方式を採用してバイトを並べるから。
いまこのコードを実行してるこのマシンが、そうなってるから。

Endianness - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

ビッグエンディアン(BE)を採用してる環境だと、見た通りの順序で読み出される。

いや、そっちのほうがわかりやすいやん!って思ったけど、CPUの処理的な都合では、LEのほうに分があったりするらしい。
ただわかりやすいのも確かなので、プロトコルなどによっては絶対BEでやりとりすると決まってたりするらしい。

もう1つ例として、`Int32Array([50462976])`が`Int8Array`になる場合を見ておく。

// 10進数から2進数にしてみる
Number(50462976).toString(2); // 11000000100000000100000000

// バイト区切り(0埋め)
00000011|00000010|00000001|00000000

// LEなので
00000000|00000001|00000010|00000011

// 10進数
0, 1, 2, 3

というわけで、`Int8Array([0, 1, 2, 3])`になる。

どっちのエンディアンを採用してるかはこういうコードでも調べられる。

const isBE = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x12;
const isLE = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x78;

DataView

DataView - JavaScript | MDN

  • 配列のインデックスで読み書きするの大変
  • エンディアンを考慮しつつ読み書きするのも大変

・・ということで存在するクラス。

`getInt8()`とか`setFloat32()`とか任意の単位で`ArrayBuffer`を操作できるやつ。

デフォルトでBEを採用してて、各メソッドの引数でLEを指定できるようになってる。

const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true /* littleEndian */);

Node.jsでいうところの`Buffer`みたいな存在。

NodeJSでBufferを読み取る - console.lealog();

まぁNode.jsだと`Buffer`も`DataView`もどっちも生えてるけど・・。