バイナリーデータの基礎知識。
JavaScriptでバイナリーデータの扱い方がよく分からなかったので、バイナリーデータ、メモリ、バッファーについて調べた基礎知識のメモ。
※ JavaScriptでバイナリーデータを扱うためのクラスには、ArrayBuffer、Blob、Fileなどがある。
バイナリーデータ
(1) バイナリーデータ
「バイナリーデータ」(Binary Data)とは、2進数で表現されるデータのこと。1bit毎に「0」か「1」で表し、それを並べた「ビット列」でデータを表現したもの。
- 「binary」の直訳は、名詞「2進数」、形容詞「2進法の」。
- 「bit」は「Binary Digit」を元にした合成語で、「2進数の桁」の意。
- 「8bits」が「1byte」。(情報技術系の単位の国際規格「IEC 80000-13」で2008年に定義。)
- 「1b」(小文字)が1bit、「1B」(大文字)が1byteとして略記の表記方法が定められている。
- 「8bits」で、2進数8桁(2^8=256通り)の表現が可能。(10進数の整数での表現なら、符号無しで「0」~「255」、符号付きで「-128」~「+127」。)
- 「8bits」が「1byte」に定まったのは、2進数なら3桁、16進数なら2桁でデータを表示できる利便性、256パターンあれば英語のアルファベット大文字小文字、数字、主要な記号文字がすべて表現できることから8-bitマシンが多く流通していたことなどの歴史的経緯によるらしい。
- 「byte」の語源は「bite」(かじる、噛む、一口)だが、「bit」との混同をさけるためにスペルが変更されている。
※ 文字コード規格「ASCII」(情報交換用米国標準コード)でのコード表記と文字の対応表。(IT用語辞典)
※ 日本産業規格(JIS)が制定している文字コード規格「JIS X 0201」でのコード表記と文字の対応表。(Wikipedia)
(2) バイナリーファイル
「バイナリーデータ」が記録されているファイルを「バイナリーファイル」という。
- 一般的に、「テキストファイル」以外のすべてのファイルを「バイナリーファイル」と定義していることが多い。
- ファイルの内容が人間可読のテキストなら「テキストファイル」、そうでなければ「バイナリーファイル」とされる。
- 2進数で表現されるデータを元に、文字コード規格に基づいて人間が読める意味のある文字で表示されるデータを「テキストデータ」、「テキストデータ」が記録されているファイルを「テキストファイル」という。
- 厳密には「テキストデータ」も「バイナリーデータ」に含まれるが、分類の便宜上、「テキストデータ」以外のことが「バイナリーデータ」とされている。
※ 具体的には、画像ファイル、動画ファイル、圧縮ファイル、ソフトウェアの実行ファイルなど、テキストファイル以外のファイルが「バイナリーファイル」となる。
(3) バイナリーエディター
テキストエディターでテキストファイルを表示・編集できるのと同じように、「バイナリーエディター」を利用すれば、「バイナリーファイル」の内容を表示・編集することができる。
- 一般的には、「バイト列」と呼ばれる16進数で2桁1byteずつの数値を配列にした状態で表示される。
- 「バイト列」で表現したときの先頭からの相対位置を示す値を、「オフセット」または「アドレス」、「バイトアドレス」などと呼ぶ。
- 内部データのフォーマット(内部構成)はファイルの形式(jpgやaviなど)ごとで規定が決まっているが、すべてのファイル形式のフォーマットの規定が一般公開されているわけではない。
- ほとんどのフォーマットは、ファイルの定義を示すメタ情報であるヘッダー部と、中身であるボディ部で構成される。
- 通常、ファイルの先頭に、ファイル形式を識別するための短いバイト列が「ファイルの署名」(File signatures)として配置されている。
- フォーマットの規定が非公開のファイル形式の場合、「バイナリーエディター」で内容を「バイト列」で表示しても、それが何を意味しているかを判別する方法は無い。
※ 「ファイルの署名」(File signatures)のリスト。(Wikipedia)
メモリ
(1) メモリ上のデータ
データがメモリに保持されるとき、データはメモリ内で1byte(8bits)単位ごとに分けて格納される。
データは、物理的に実在するメモリに物理的に格納されるので、メモリからデータの読み書きをする場合は、格納されている位置(メモリアドレス)や長さ(bit数)を正しく指定してアクセスする必要がある。
(2) メモリアドレス
メモリ内の一意に定まる格納場所を示すための値を「メモリアドレス」という。(1byte単位で「メモリアドレス」が一意に定まっている。)
- メモリ上のデータへアクセスする場合、そのデータが格納されている「メモリアドレス」を指定する。
- 1byteのデータなら1つのアドレス、2bytesのデータなら連続した2つのアドレスの場所を使用して格納され、一番小さいアドレスがそのデータのアドレスとなる。
- 通常、アプリケーションごとに一続きの「メモリアドレス」になるように「仮想的なメモリアドレス」が使われ、OSの内部処理によって実際の「物理的なメモリアドレス」に変換されて動作している。
- 「メモリアドレス」によってアクセスできるメモリ内の領域を「アドレス空間」という。
(3) アドレスバス
「アドレスバス」とは、メモリアドレス指定のための信号を送信する信号線。(CPUとメモリを物理的に繋いでいる線。)
- 1本ごとに「on/off」の信号を発信するので、2進数でアドレスを指定できる。
- 信号1本あたり1bitを使って発信することになるので、「アドレスバス」の上限数はCPUのbit数に依存する。(8bitsなら8本、32bitsなら32本、64bitsなら64本。)
- 8bitsなら8本の「on/off」なので2^8=256通りのアドレスを指定でき、32bitsなら2^32=4,294,967,296通り指定できる。(32-bitのCPUのメモリ上限が4GBと言われていた理由。)
(4) データバス
「データバス」とは、CPUとメモリ間でデータそのものを物理的に伝送する信号線。
- 「データバス」が一度に送信できるbit数を「データバス幅」という。
- 「データバス幅」がCPUが一括処理できるbit数となり、この一括処理できるbit数をCPUの「ワード」という。
(5) エンディアン (バイトオーダー)
2bytes以上(マルチバイト)のデータはメモリ内のひとつのメモリアドレス(1byte)には納められないので、連続したアドレスに1byteごとに格納される。
このときの1byte単位の並び順のことを「エンディアン」(Endianness)や「バイトオーダー」(Byte Order)という。
※ 「エンディアン」の方式はCPUによって決まっている。(メーカーにより規定されている。)
- 「ビッグエンディアン」 : 1バイトずつ、最上位(通常左側)から順に格納。
- 「リトルエンディアン」 : 1バイトずつ、最下位(通常右側)から順に格納。
- 「バイエンディアン」 : 両方のエンディアンに対応。
(エンディアンの例)
16進数で表示すると「1234ABCD」となる4bytes(32bits)のデータの場合。
・「ビッグエンディアン」 : 「12 34 AB CD」の順でメモリに格納。
・「リトルエンディアン」 : 「CD AB 34 12」の順でメモリに格納。
(6) アライメント
「アライメント」とは、メモリにデータを格納するとき、CPUの「ワード」(一括処理できるbit数)を考慮して配置処理すること。(データを配置するメモリアドレスを調整すること。)
- CPUはワード単位でメモリに物理的にアクセスするので、データが正しくアラインされていなければ1回のアクセスでデータを読み書きできず、パフォーマンスに影響が出る。
- 2bytes以上のデータはバイト単位で並べてメモリ内に格納されるので、CPUが1回で処理するワード内に収まるように配置を考慮する必要がある。(配置によっては、ひとつのデータのアクセスのために2回の物理処理が発生してしまう。)
- 「アライメント」は、CPUがメモリにアクセスするときの物理的な制約に起因する処理効率の問題で、「アライメント」を考慮しないとエラーになったり処理速度が低下したりする。
- 装置の設計により「アライメント」に寛容であったり、プログラミング言語が自動的に「アライメント」の処理をしてくれたりする場合もある。
[ 32bit-CPUの場合の例 ]
- CPUがメモリへ物理的なアクセスをするとき、ワードのbit数単位でアクセスする。
- ワードが32bitsなら4bytes(32bits)単位でメモリアドレスにアクセスする。(メモリアドレスの「0番~3番」、「4番~7番」、「8番~11番」…。)
- ひとつのデータのバイト列すべてが、1回でアクセスできるメモリ4bytesの並び(「0番~3番」など)の中に収まっていれば、物理処理が1回で済む。
- このようにメモリアクセスを効率化するために、ひとつのデータの先頭のメモリアドレスがnの倍数(ここでは4の倍数)になるように格納することを「4バイト境界にアラインする」、「4バイトアライメントする」などという。
- この環境でもし2bytesのデータがメモリアドレスの「3番」と「4番」を使って配置された場合、「4バイト境界をまたぐ」という。(メモリアドレスの「0番~3番」と「4番~7番」にアクセスするため、2回の物理的な処理が必要になる。)
- データが境界をまたぐ場合、期待した動作にならない可能性がある。(処理速度が低下する、メモリアドレスが自動的に変更される、エラーになるなど。)
バッファー
(1) バッファー
「バッファー」とは、メモリ内でバイナリーデータが格納されている領域のこと。(データを一時的に保持するために使用されるメモリ内の、そのデータが存在している領域。)
(2) JavaScriptでのバッファー
JavaScriptでの「バッファー」は、メモリに格納されているバイナリーデータを参照するオブジェクトのことを指し、「ArrayBuffer」オブジェクトで実装されている。
- 「ArrayBuffer」オブジェクト(バッファー)は、物理メモリに指定サイズの領域を確保し、そこにバイナリーデータをバイト配列で格納する。
- 「ArrayBuffer」オブジェクトの中身は直接読み書きできない。(データの内容を直接知ることはできない。)
- 中身を読み書きするには、「ArrayBuffer」オブジェクトのバイト配列を別の形式に変換して参照する「ビュー」(view)を利用し、「ビュー」によって「ArrayBuffer」オブジェクトの内容を操作する。
- 「ビュー」とは、具体的には、「型付き配列」(TypedArray)オブジェクトまたは「DataView」オブジェクトのこと。
(3) ArrayBufferオブジェクト
「ArrayBuffer」オブジェクトは、指定したバイトサイズ分の物理メモリへの参照。
※ ArrayBufferについてのMozillaの公式ドキュメント。
- 「ArrayBuffer」オブジェクトの参照(バッファー)は、バイナリーデータの塊(バイトの配列)を表す。(他の言語では「バイト配列」と呼ばれる。)
- 「ArrayBuffer」オブジェクトを生成した時点で、指定したバイトサイズ分のメモリアドレスの領域が物理メモリ内に確保される。
- 指定したバイト数が「n」なら、連続するメモリアドレスn個分の領域をメモリ内に確保し、その領域を参照する。
- 生成した時点で、長さ(バイトサイズ)は固定となり、増減することはできない。
- 「ArrayBuffer」オブジェクト自体には特に形式がなく、また中身を直接操作することはできない。(バイト配列が存在するだけ。)
- 「ArrayBuffer」オブジェクト自体には何かができるプロパティーやメソッドはほぼ無いので、中身の読み書きには「ビュー」(view)を利用する必要がある。(「ビュー」には大別して、「型付き配列」(TypedArray)と「DataView」がある。)
- バッファーの内容を読み書きするには、存在する「ArrayBuffer」オブジェクトを元にして、「ビュー」オブジェクトを作成する。(「ビュー」によって新たなバッファーが生成されるわけではなく、同一のバッファーの内容を操作する。)
- 存在する「ArrayBuffer」オブジェクトを元にせず、新規で「ビュー」オブジェクトを作成した場合、内部的に自動で「ArrayBuffer」オブジェクトも生成されている。
- 既存のバイナリーデータ(Base64文字列やPC内のローカルファイルなど)から配列バッファーを取得することもできる。(File APIの「FileReader」などは読み込んだファイルデータを「ArrayBuffer」オブジェクトで返す。)
[ ビュー ]
「ビュー」の明確な定義がドキュメントから見つけられないので、「ビュー」自体の定義の詳細は不明。
※ バッファーとビューについてのMozillaの公式ドキュメント。
- 「ビュー」は、コンテキスト(データの種類、開始位置のオフセット、要素の数)を提供する。
- 「型付き配列」の「ビュー」は、自身を表現する名称を持つ。(Int8など。)
- Int8、Uint32、Float64などの一般的な数値型の「ビュー」以外に、画像処理アルゴリズム(Canvasなど)を扱うときに有用なUint8ClampedArrayなど特別な「ビュー」もある。
「ビュー」とは、おそらく下記のようなことだと思われる。
- 「ArrayBuffer」オブジェクトは、メモリにバッファーを確保し、それを参照する以外の特別な機能はほぼ無い。
- JavaScriptでバッファーの生のデータを扱うためには、そのデータを参照して操作するための機能を持ったオブジェクトを実装する必要がある。
- このために策定・実装されたオブジェクトのことを「ビュー」と呼び、データを参照するときは常に指定した型の状態に変換するオブジェクトが「型付き配列」、型が事前には指定されていないオブジェクトが「DataView」として実装されている。(?)
[ ArrayBuffer()コンストラクター ]
ArrayBuffer()コンストラクターで16bytesのバイナリーデータのバッファーを生成する例。
- new演算子を付加し、引数に「任意のバイトサイズ」を指定してArrayBuffer()コンストラクターを呼び出す。
- 中身(バイナリデータ)が「0」で初期化されたバッファーが生成される。
※ new演算子を指定せずに関数としてArrayBuffer()コンストラクターを呼び出すと、TypeErrorが発生する。
※ 指定したバイトサイズの領域を物理メモリに確保できない場合はエラーになる。
//16bytesのバッファーを作成
const ag2buffer = new ArrayBuffer(16);
//byteLengthプロパティーでバッファーの総バイトサイズを確認
console.log(ag2buffer.byteLength);
//出力されるコンソールの内容
//16
//slice()メソッドで一部をコピーして新しいバッファーを生成
const newAg2buffer = ag2buffer.slice(1, 5);
console.log(newAg2buffer.byteLength);
//出力されるコンソールの内容
//4
new ArrayBuffer(バイトサイズ) : 指定したバイトサイズのArrayBufferオブジェクトを生成して返す。生成されるバッファーの中身(バイナリデータ)は、すべて「0」で初期化されている。
ArrayBuffer.byteLength : 読み取り専用プロパティー。ArrayBufferオブジェクト(バッファー)の総バイトサイズを返す。
ArrayBuffer.slice(起点位置, 終点位置) : 指定したArrayBufferオブジェクト(バッファー)の、指定した起点位置から指定した終点位置の直前までのデータをコピーし、新しいArrayBufferオブジェクトとして生成して返す。位置はバイトインデックスで指定する。(終点位置を省略した場合、指定した起点位置から最後までが取得される。)
(4) 型付き配列(TypedArray)オブジェクト
「型付き配列」(TypedArray)は配列状のオブジェクトで、物理メモリ内のバイナリーデータにアクセスする手段を提供する。
※ 型付き配列についてのMozillaの公式ドキュメント。
- 「型付き配列」は、バックエンドで内部的に処理される「バッファー」(ArrayBufferオブジェクト)と、その「バッファー」にアクセスするための「ビュー」で実装が分けられている。
- C言語の配列のようにメモリの「バッファー」に対して型ビューを使ってアクセスする。
- 「バッファー」を参照し、その中身(バイナリデータ)を「ビュー」によって指定した数値型などで表現することで、「バッファー」の内容を読み書きする。
- 「ビュー」によって新たなバッファーが生成されるわけではなく、同一のバッファーの内容を操作する。
- 既存の「バッファー」(ArrayBufferオブジェクト)を元にせず「ビュー」オブジェクトを新しく生成した場合、自動的に内部処理でArrayBufferオブジェクトも生成される。
- 「型付き配列」の「ビュー」は、動作しているCPUネイティブの「エンディアン」(バイトオーダー)になるので、プログラムの利用環境に応じた「エンディアン」を考慮する必要がある。
- 低機能の代わりに高速で処理できる。
- 「型付き配列」は通常の配列のようにインデックスがあり、反復可能(iterable)だが、「Array」オブジェクトと全く同じではないので、「型付き配列.isArray()」は「false」を返す。(「Array」オブジェクトのメソッドがすべて使えるわけではない。)
-
「型付き配列」は生のメモリにデータが保存されるため、JavaScriptエンジンはネイティブライブラリーに直接メモリ上のデータを渡すことができる。(ネイティブな表現に変換する必要はない。)
- 「型付き配列」は、WebGLで効率的にバイナリーデータを扱う方法の必要性から生まれたもの。
[ TypedArray()コンストラクター ]
TypedArray()コンストラクターで「型付き配列」オブジェクトを生成する例。
- 任意の型のTypedArray()コンストラクター(ここでは「Uint8Array()コンストラクター」)を指定。
- new演算子を付加し、引数に「任意のバイトサイズ」を指定して呼び出す。
- 中身(バイナリデータ)が「0」で初期化されたバッファーが生成される。
※ 「Uint8Array」は、バッファーのデータのそれぞれの1byteを「0」~「255」の整数で表現して扱う。この数値を「8ビット符号なし整数」(8-bit unsigned integer)と呼ぶ。(格納されているデータを1byteごとに10進数で表現。)
※ 引数には、「既存のバッファー」、「任意のバイト配列」、「他のビューオブジェクト」を指定することもできる。
※ 内部で「ArrayBuffer」オブジェクトも生成される。(「buffer」プロパティーで参照できる。)
//バイトサイズを指定してUint8Arrayオブジェクトを作成
let ag2uint8View = new Uint8Array(8);
console.log(ag2uint8View);
//出力されるコンソールの内容
//Uint8Array(8) [ 0, 0, 0, 0, 0, 0, 0, 0 ]
//配列で初期データを指定する場合(配列の要素数がバッファーのバイトサイズになる)
let ag2uint8View = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
console.log(ag2uint8View);
//出力されるコンソールの内容
//Uint8Array(10) [ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ]
//データ内容を書き換え
ag2uint8View[2] = 100;
console.log(ag2uint8View);
//出力されるコンソールの内容
//Uint8Array(10) [ 9, 8, 100, 6, 5, 4, 3, 2, 1, 0 ]
new Uint8Array(バイトサイズ) : 指定したバイトサイズの8ビット符号なし整数値の配列で「型付き配列」オブジェクトを生成して返す。中身はすべて「0」で初期化されている。引数を指定しない場合、長さ0の「型付き配列」オブジェクトを返す。(「バイトサイズ」の代わりに「既存のバッファー」、「任意のバイト配列」、「他のビューオブジェクト」を指定することもできる。)
[ その他の型の例 ]
- Uint16Array : 各2bytesを「0」~「65535」の整数で表現して扱う。この数値を「16ビット符号なし整数」と呼ぶ。
- Uint32Array : 各4bytesを「0」~「4294967295」の整数で表現して扱う。この数値を「32ビット符号なし整数」と呼ぶ。
- Float64Array : 各8bytesを「5.0×10-324」~「1.8×10308」の浮動小数点で表現して扱う。この数値を「64ビット浮動小数点数」と呼ぶ。
- Uint8ClampedArray : 各1byteを「0」~「255」の範囲内の整数に丸めて表現して扱う。「0」以下の数値は「0」、「255」以上の数値は「255」に変更する。(8ビットの範囲からオーバーフローした値を手動で修正する必要がなくなる。)
(5) DataViewオブジェクト
「DataView」は型指定のない「ビュー」で、あらゆるフォーマットのデータにアクセスすることができる。
※ DataViewについてのMozillaの公式ドキュメント。
- 「DataView」は、ArrayBufferの多様な数値型を、プラットフォームのエンディアンに関係なく読み書きするための低水準インターフェイスを提供する。
- 「ビュー」のフォーマットは、「DataView」のコンストラクタではなくメソッドでの呼び出し時に指定する。
- デフォルトでのエンディアン方式はビッグエンディアンで、メソッドを利用する事でリトルエンディアンに設定することができる。
- 高機能の代わりに処理は低速。
- アライメント(バイト境界)を考慮する必要はなく、エラーが発生するのは、アクセスを試みた領域がバッファーの総サイズを超えていた時だけ。
- 「DataView」は、特にネットワークI/Oのように常に指定されたエンディアンを持つデータのために設計されている。(最大のパフォーマンスを得るためのアライメントがされてない場合がある。)
[ DataView()コンストラクター ]
DataView()コンストラクターでバッファーの内容を読み書きする例。
- new演算子を付加し、引数に任意のバッファーを指定してDataView()コンストラクターを呼び出す。
- 「set()」メソッドで型を指定してデータの内容を書き換える。
- 「get()」メソッドで型を指定してデータの内容を取得する。
※ 「DataView」は「型付き配列」と違い、新規のバッファーを作成できないので、既存のバッファーを指定する必要がある。
//8bytesのバッファーを作成
let ag2buffer = new ArrayBuffer(8);
//DataViewでバッファーにアクセス
let ag2DataView = new DataView(ag2buffer);
console.log(ag2DataView);
//出力内容
//DataView { buffer: ArrayBuffer, byteLength: 8, byteOffset: 0 }
//setInt8()メソッドでデータ内容を書き換え
ag2DataView.setInt8(1, 100);
//getInt8()メソッドでデータ内容を取得
let myData = ag2DataView.getInt8(1);
console.log(myData);
//出力内容
//100
//型付き配列で内容を確認
let ag2uint8View = new Uint8Array(ag2buffer);
console.log(ag2uint8View);
//Uint8Array(8) [ 0, 100, 0, 0, 0, 0, 0, 0 ]
new DataView(バッファー, 開始位置, バイトサイズ) : 指定したバッファー(ArrayBufferオブジェクト)の指定した開始位置から指定したサイズの「DataView」オブジェクトを返す。
- Endianってなに?
- データ型のアラインメントとは何か,なぜ必要なのか?
- ArrayBuffer, binary arrays
- Typed Arrays: ブラウザでバイナリデータを扱う
- 型付き配列について(TypedArray)
https://memo.ag2works.tokyo/post-4174/
バイナリーデータの基礎知識。 | memo メモ [AG2WORKS]
<a href="https://memo.ag2works.tokyo/post-4174/" target="_blank" rel="noopener">バイナリーデータの基礎知識。 | memo メモ [AG2WORKS]</a>
この記事へのコメント
コメントの書き込みはまだありません。