1. はじめに
センサなどの各種外部デバイスや通信では、多くの数値データを取り扱います。その際、主に配列を使ってデータ処理を行います。第6回は、JavaScriptの「配列」、ArrayとArrayBuffer/TypedArrayにフォーカスし、neqto.jsを使用して詳しく解説していきます。
「neqto.js」は、NEQTO Engine上で動作するJavaScript環境です。ECMAScript 5.1 Edition (ES5.1)に対応しています。なお、ArrayBufferとTypedArrayは ECMAScript 2015 (ES6)で定義されている機能ですが、NEQTO Engineでは、このArrayBufferとTypedArrayの一部主要機能が利用可能です。
2. 「Array」
はじめに、Arrayについてみていきます。
インスタンス作成
まずは基本的なArrayのインスタンスを作成する方法からみていきます。
一つ目の方法は、new Array()コンストラクタを使い、Arrayインスタンスを作成する方法です。
この方法の場合、要素すべての初期値はundefinedとなります。要素数は.lengthプロパティで確認できます。
下記の例では、4つの要素を持った配列を作成し、各要素に値を代入します。
var arr = new Array(4);
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
print('After:');
arr[0] = 1;
arr[1] = 2;
arr[2] = 4;
arr[3] = 8;
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
[0]: undefined
[1]: undefined
[2]: undefined
[3]: undefined
After:
[0]: 1
[1]: 2
[2]: 4
[3]: 8
もう一つの方法は、配列リテラル([])を使う方法です。
この方法では、Arrayインスタンスの作成と、任意初期値の代入を同時に行います。
var arr = [1, 2, 4, 8];
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
[0]: 1
[1]: 2
[2]: 4
[3]: 8
値のデータ型
Arrayの各要素には、数値や文字列以外に、関数やオブジェクトなども入れることができます。
var arr = [123, 'foo', true, {'bar': null}];
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
[0]: 123
[1]: foo
[2]: true
[3]: [object Object]
要素の追加と削除
Arrayは、要素の追加や削除が可能です。
代表的なpush()、pop()、shift()、unshift()メソッドの使用例をみていきます。
push()は、配列の末尾に要素を追加し、新しい配列の長さを返しますpop()は、配列から末尾の要素を取り除き、その値を返しますshift()は、配列から先頭の要素を取り除き、その値を返しますunshift()は、配列の先頭に要素を挿入し、新しい配列の長さを返します
var arr = [1, 2, 4, 8];
print('[' + arr.join(',') + ']');
arr.push(16);
print('push:');
print('[' + arr.join(',') + ']');
arr.pop()
print('pop:');
print('[' + arr.join(',') + ']');
arr.shift();
print('shift:');
print('[' + arr.join(',') + ']');
arr.unshift(0);
print('unshift:');
print('[' + arr.join(',') + ']');
[1,2,4,8]
push:
[1,2,4,8,16]
pop:
[1,2,4,8]
shift:
[2,4,8]
unshift:
[0,2,4,8]
配列のコピー
配列を複製する場合、単純に代入式(dstArr = srcArr)ではコピーとなりません。
Arrayは数値のような「プリミティブ」ではなく「オブジェクト」であるため、代入式(dstArr = srcArr)を使用した場合、代入先(dstArr)からは、代入元(srcArr)のインスタンスを単に参照する形となります。すなわち、srcArrの要素を変更すると、dstArrからも同じようにみえます。逆にdstArrの要素を変更すると、srcArrからも同じようにみえます。
var srcArr = [1, 2, 4, 8];
print('srcArr:');
print('[' + srcArr.join(',') + ']');
var dstArr = srcArr;
print('dstArr:');
print('[' + dstArr.join(',') + ']');
print('After:');
dstArr[0] = 0;
print('srcArr:');
print('[' + srcArr.join(',') + ']');
print('dstArr:');
print('[' + dstArr.join(',') + ']');
srcArr:
[1,2,4,8]
dstArr:
[1,2,4,8]
After:
srcArr:
[0,2,4,8]
dstArr:
[0,2,4,8]
配列の実体(インスタンス)を新たなインスタンスとしてコピーするには、slice()やconcat()メソッドを活用します。
slice()は、配列の一部もしくはすべてを、新たに作成したArrayインスタンスにコピーしますconcat()は、複数の配列を、新たに作成したArrayインスタンスに結合します
下記は、slice()メソッドを使用した例です。
コピー元(srcArr)とコピー先(dstArr)は別インスタンスであるため、dstArrを変更したとしてもsrcArrは影響を受けません。
var srcArr = [1, 2, 4, 8];
print('srcArr:');
print('[' + srcArr.join(',') + ']');
var dstArr = srcArr.slice(); //copy
print('dstArr:');
print('[' + dstArr.join(',') + ']');
print('After:');
dstArr[0] = 0;
print('srcArr:');
print('[' + srcArr.join(',') + ']');
print('dstArr:');
print('[' + dstArr.join(',') + ']');
srcArr:
[1,2,4,8]
dstArr:
[1,2,4,8]
After:
srcArr:
[1,2,4,8]
dstArr:
[0,2,4,8]
下記は、concat()メソッドを使用した例です。
コピー元(srcArr0、srcArr1)と結合先(dstArr)は別インスタンスであるため、dstArrを変更したとしてもsrcArr0およびsrcArr1は影響を受けません。
var srcArr0 = [1, 2, 4, 8];
var srcArr1 = [16, 32, 64, 128];
var dstArr = srcArr0.concat(srcArr1);
print('srcArr0:');
print('[' + srcArr0.join(',') + ']');
print('srcArr1:');
print('[' + srcArr1.join(',') + ']');
print('dstArr:');
print('[' + dstArr.join(',') + ']');
print('After:');
dstArr[0] = 0;
dstArr[4] = 0;
print('srcArr0:');
print('[' + srcArr0.join(',') + ']');
print('srcArr1:');
print('[' + srcArr1.join(',') + ']');
print('dstArr:');
print('[' + dstArr.join(',') + ']');
srcArr0:
[1,2,4,8]
srcArr1:
[16,32,64,128]
dstArr:
[1,2,4,8,16,32,64,128]
After:
srcArr0:
[1,2,4,8]
srcArr1:
[16,32,64,128]
dstArr:
[0,2,4,8,0,32,64,128]
3. 「ArrayBuffer / TypedArray」
はじめに、ArrayBufferとTypedArrayの関係について簡単に説明します。
ArrayBufferは、バッファ(メモリ)を確保するために使用され、データ型を持ちません。したがって、このバッファは直接参照できません。TypedArrayは、このバッファにデータの型を与えて、配列を通して使用できるようにします。TypedArrayとしては、符号なし8ビットデータ用のUint8Arrayや符号付き32ビットデータ用のInt32Arrayなど、データ型に応じた型が具体的に使用されます。
インスタンス作成
まずはArrayBufferとTypedArrayのインスタンスを作成する方法からみていきます。
一つ目の方法は、new ArrayBuffer()コンストラクタでバッファを確保し、new TypedArray()コンストラクタを通して参照する方法です。なお、バッファは0x00で初期化されます。バッファサイズは.byteLengthプロパティで確認できます。TypedArrayの要素数は.lengthプロパティで確認できます。
下記の例では、4バイトのバッファを作成し、Uint8Arrayを適用して、各要素に値を代入します。
var buf = new ArrayBuffer(4);
var arr = new Uint8Array(buf);
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
print('After:');
arr[0] = 1;
arr[1] = 2;
arr[2] = 4;
arr[3] = 8;
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
[0]: 0
[1]: 0
[2]: 0
[3]: 0
After:
[0]: 1
[1]: 2
[2]: 4
[3]: 8
もう一つの方法は、TypedArrayの引数に任意の初期値を与えて作成する方法です。
引数に指定されたデータを格納するためのバッファが確保され、その値で初期化されます。
なお、暗黙に作成されるArrayBufferインスタンスには、.bufferプロパティからアクセス可能です。
var arr = new Uint8Array([1, 2, 4, 8]);
for(var i = 0; i < arr.length; i++) {
print('['+i+']:', arr[i]);
}
print('buffer size:', arr.buffer.byteLength);
[0]: 1
[1]: 2
[2]: 4
[3]: 8
buffer size: 4
値のデータ型
NEQTO Engineで使用可能なTypedArrayのデータ型を下記に示します。
| TypedArray | Range | Bits | Bytes | DataType |
|---|---|---|---|---|
| Int8Array | -128 to 127 |
8 | 1 | 符号付き整数 |
| Uint8Array | 0 to 255 |
8 | 1 | 符号なし整数 |
| Int16Array | -32768 to 32767 |
16 | 2 | 符号付き整数 |
| Uint16Array | 0 to 65535 |
16 | 2 | 符号なし整数 |
| Int32Array | -2147483648 to 2147483647 |
32 | 4 | 符号付き整数 |
| Uint32Array | 0 to 4294967295 |
32 | 4 | 符号なし整数 |
| Float32Array | -3.4\*10^38 to 3.4\*10^38 |
32 | 4 | 浮動小数点 |
| Float64Array | -1.8\*10^308 to 1.8\*10^308 |
64 | 8 | 浮動小数点 |
それぞれのデータ型には範囲がありますが、範囲外の値を代入したとしてもエラーとならないことに注意が必要です。範囲外のデータビットは切り捨てられます。
Uint8Arrayの配列に範囲外の値を代入した場合、下記のようになります。
var arr = new Uint8Array([0]); //Range: 0 to 255(0xFF)
arr[0] = 257; print(arr[0]); //1
arr[0] = 256; print(arr[0]); //0
arr[0] = 255; print(arr[0]); //255
arr[0] = 254; print(arr[0]); //254
arr[0] = 2; print(arr[0]); //2
arr[0] = 1; print(arr[0]); //1
arr[0] = 0; print(arr[0]); //0
arr[0] = -1; print(arr[0]); //255
arr[0] = -2; print(arr[0]); //254
また、ArrayBufferは異なるTypedArrayのデータ型で参照することが可能です。
下記の例では、Uint32Arrayで作成した配列を、Uint8Array型で参照します。
このように、異なるバイトサイズで参照する場合は、バイト順序(エンディアン)に注意が必要です。
var arrUint32 = new Uint32Array([0x01020408, 0x10204080]);
print('Uint32Array:')
for(var i = 0; i < arrUint32.length; i++) {
print('['+i+']: 0x'+arrUint32[i].toString(16));
}
var arrUint8 = new Uint8Array(arrUint32.buffer);
print('Uint8Array:')
for(var i = 0; i < arrUint8.length; i++) {
print('['+i+']: 0x'+arrUint8[i].toString(16));
}
Uint32Array:
[0]: 0x1020408
[1]: 0x10204080
Uint8Array:
[0]: 0x8
[1]: 0x4
[2]: 0x2
[3]: 0x1
[4]: 0x80
[5]: 0x40
[6]: 0x20
[7]: 0x10
要素の追加と削除
TypedArrayを適用した配列は、元になるArrayBufferが固定長であるため、要素の追加や削除できません。 そのため、要素数を変更する場合は、ArrayBufferを新規に作り直す必要があります。
配列のコピー
TypedArrayもArray同様に、代入式(dstArr = srcArr)ではコピーとなりません。
function arrDump(arr) {
var str = '[';
for(var i = 0; i < arr.length; i++) {
if(i) str += ',';
str += arr[i];
}
str += ']';
print(str);
}
var srcArr = new Uint8Array([1, 2, 4, 8]);
var dstArr = srcArr;
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
print('After:');
dstArr[0] = 0;
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
srcArr:
[1,2,4,8]
dstArr:
[1,2,4,8]
After:
srcArr:
[0,2,4,8]
dstArr:
[0,2,4,8]
ArrayBufferも同様に、代入式(dstBuf = srcBuf)ではコピーとなりません。
var srcBuf = new ArrayBuffer(4);
var dstBuf = srcBuf;
var srcArr = new Uint8Array(srcBuf);
var dstArr = new Uint8Array(dstBuf);
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
print('After:');
dstArr[0] = 1;
dstArr[1] = 2;
dstArr[2] = 4;
dstArr[3] = 8;
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
srcArr:
[0,0,0,0]
dstArr:
[0,0,0,0]
After:
srcArr:
[1,2,4,8]
dstArr:
[1,2,4,8]
配列の実体(インスタンス)を新たなインスタンスとしてコピーするには、ArrayBufferのslice()メソッドを活用します。
slice()は、バッファの一部もしくはすべてを、新たに作成したバッファにコピーします
下記は、slice()メソッドを使用した例です。
コピー元(srcArr)とコピー先(dstArr)は別インスタンスであるため、dstArrを変更したとしてもsrcArrは影響を受けません。
var srcArr = new Uint8Array([1, 2, 4, 8]);
var srcBuf = srcArr.buffer;
var dstBuf = srcBuf.slice(); //copy
var dstArr = new Uint8Array(dstBuf);
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
print('After:');
dstArr[0] = 0;
print('srcArr:');
arrDump(srcArr);
print('dstArr:');
arrDump(dstArr);
srcArr:
[1,2,4,8]
dstArr:
[1,2,4,8]
After:
srcArr:
[1,2,4,8]
dstArr:
[0,2,4,8]
ArrayBuffer/TypedArrayは結合のメソッドを持ちません。結合が必要な場合は、新たに必要な長さのバッファを確保し、データを代入します。
var srcArr0 = new Uint8Array([1, 2, 4, 8]);
var srcBuf0 = srcArr0.buffer;
var srcArr1 = new Uint8Array([16, 32, 64, 128]);
var srcBuf1 = srcArr1.buffer;
var dstBuf = new ArrayBuffer(srcBuf0.byteLength + srcBuf1.byteLength);
var dstArr = new Uint8Array(dstBuf);
var i = 0;
for(var j = 0; j < srcArr0.length; j++) {
dstArr[i++] = srcArr0[j];
}
for(var j = 0; j < srcArr1.length; j++) {
dstArr[i++] = srcArr1[j];
}
print('srcArr0:');
arrDump(srcArr0);
print('srcArr1:');
arrDump(srcArr1);
print('dstArr:');
arrDump(dstArr);
srcArr0:
[1,2,4,8]
srcArr1:
[16,32,64,128]
dstArr:
[1,2,4,8,16,32,64,128]
4. 「Array」と「ArrayBuffer / TypedArray」の処理速度
ここまで、ArrayとArrayBuffer/TypedArrayそれぞれの使い方をみてきました。
双方を比べた場合、一見、Arrayの方が使い勝手が良いようにみえますが、ArrayBuffer/TypedArrayは、固定長でかつ数値のみを扱うがゆえに処理速度が速いという特徴があります。
実際に、配列へ数値を代入して、その数値の合計を演算するためにかかる時間を計測してみます。
var arrLen = 1024;
print('Calculation time:');
//Array:
for(var j = 0; j < 3; j++) {
var startTime = Date.now();
var arr = Array(arrLen);
for(var i = 0; i < arr.length; i++) {
arr[i] = i % 200;
}
for(var i = 0, sum = 0; i < arr.length; i++) {
sum += arr[i];
}
print('Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum);
arr = undefined;
}
//Uint8Array:
for(var j = 0; j < 3; j++) {
var startTime = Date.now();
var arr = new Uint8Array(arrLen);
for(var i = 0; i < arr.length; i++) {
arr[i] = i % 200;
}
for(var i = 0, sum = 0; i < arr.length; i++) {
sum += arr[i];
}
print('Uint8Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
arr = undefined;
}
//Float64Array:
for(var j = 0; j < 3; j++) {
var startTime = Date.now();
var arr = new Float64Array(arrLen);
for(var i = 0; i < arr.length; i++) {
arr[i] = i % 200;
}
for(var i = 0, sum = 0; i < arr.length; i++) {
sum += arr[i];
}
print('Float64Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
arr = undefined;
}
下記は、「STM32 Discovery Kit」を使用して計測した結果です。
Calculation time:
Array: 2269 [ms] , length: 1024 , Sum: 99776
Array: 2262 [ms] , length: 1024 , Sum: 99776
Array: 2266 [ms] , length: 1024 , Sum: 99776
Uint8Array: 465 [ms] , length: 1024 , Sum: 99776 , byteLen: 1024
Uint8Array: 469 [ms] , length: 1024 , Sum: 99776 , byteLen: 1024
Uint8Array: 469 [ms] , length: 1024 , Sum: 99776 , byteLen: 1024
Float64Array: 472 [ms] , length: 1024 , Sum: 99776 , byteLen: 8192
Float64Array: 457 [ms] , length: 1024 , Sum: 99776 , byteLen: 8192
Float64Array: 457 [ms] , length: 1024 , Sum: 99776 , byteLen: 8192
測定結果より下記の特徴がわかります。
Arrayと比べ、TypedArrayの方が処理時間がかからない (今回の例では約1/4)TypedArrayのバイトサイズ(Bytes)によって処理時間は変わらないTypedArrayのバイトサイズを小さくする(使用する数値の範囲を小さくする)ことで、使用メモリが抑制される
※Arrayで数値を扱った場合、Float64同等となる
5. メモリの解放
JavaScriptではメモリ(バッファ)の確保および解放を意識する必要はありません。
しかしながら、下記の例のように一時的に大きなメモリを確保する処理を繰り返すと、メモリ不足が発生する場合があります。その際、不要となった(使用済み)バッファを意図的にundefined化することで、メモリ不足を回避できる可能性があります。
var buf0 = new ArrayBuffer(16384);
var arr0 = new Uint8Array(buf0);
// do something...
buf0 = arr0 = undefined; //free
var buf1 = new ArrayBuffer(16384);
var arr1 = new Uint8Array(buf1);
// do something...
buf1 = arr1 = undefined; //free
var buf2 = new ArrayBuffer(16384);
var arr2 = new Uint8Array(buf2);
// do something...
6. まとめ
いかがでしたか。Arrayは要素の追加や削除が可能なことから柔軟性が高く、例えば可変長のコマンドデータを処理する場合に向いています。一方、ArrayBufferはArrayのように要素の追加や削除はできませんが、処理速度が速く、また、バイト(8ビット)単位の固定長データを扱う場合は、Arrayよりも使用メモリ量を抑制できます。例えば固定長のバイナリフレームのコマンドを処理する場合に向いています。それぞれの特徴を理解し、適切な配列を選択することは、効率的なプログラムの構築に役立ちます。ぜひご参考にしてください。
リンク
Standard ECMA-262 5.1 Edition ECMAScript® Language Specification - 15.4 Array Objects
Standard ECMA-262 5.1 Edition ECMAScript® Language Specification - 22.2 TypedArray Objects
Standard ECMA-262 5.1 Edition ECMAScript® Language Specification - 24.1 ArrayBuffer Objects