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