本記事のポイント

本ブログはIoT-CoreエンジンNEQTOの特徴の一つであるJavaScriptによるNEQTO Engine対応デバイスの開発にフォーカスし、JavaScriptの基礎およびneqto.js特有の拡張機能について連載形式で紹介していきます。

この連載記事では、JavaScriptの基礎となる構文や文法について順番に解説していきます。第6回目の今回は「配列」について解説します。NEQTOユーザー様がJavaScript開発を円滑に進めるために活用頂くこと、また、NEQTOに興味をお持ちの方がNEQTOを使用したJavaScript開発について理解を深めて頂くことが本ブログの目的となります。



1. はじめに

センサなどの各種外部デバイスや通信では、多くの数値データを取り扱います。その際、主に配列を使ってデータ処理を行います。第6回は、JavaScriptの「配列」、ArrayArrayBuffer/TypedArrayにフォーカスし、neqto.jsを使用して詳しく解説していきます。

「neqto.js」は、NEQTO Engine上で動作するJavaScript環境です。ECMAScript 5.1 Edition (ES5.1)に対応しています。なお、ArrayBufferTypedArrayは ECMAScript 2015 (ES6)で定義されている機能ですが、NEQTO Engineでは、このArrayBufferTypedArrayの一部主要機能が利用可能です。

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()メソッドを使用した例です。
コピー元(srcArr0srcArr1)と結合先(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」

はじめに、ArrayBufferTypedArrayの関係について簡単に説明します。
ArrayBufferは、バッファ(メモリ)を確保するために使用され、データ型を持ちません。したがって、このバッファは直接参照できません。TypedArrayは、このバッファにデータの型を与えて、配列を通して使用できるようにします。TypedArrayとしては、符号なし8ビットデータ用のUint8Arrayや符号付き32ビットデータ用のInt32Arrayなど、データ型に応じた型が具体的に使用されます。


インスタンス作成


まずはArrayBufferTypedArrayのインスタンスを作成する方法からみていきます。
一つ目の方法は、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を新規に作り直す必要があります。


配列のコピー


TypedArrayArray同様に、代入式(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]

配列の実体(インスタンス)を新たなインスタンスとしてコピーするには、ArrayBufferslice()メソッドを活用します。

  • 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」の処理速度

ここまで、ArrayArrayBuffer/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は要素の追加や削除が可能なことから柔軟性が高く、例えば可変長のコマンドデータを処理する場合に向いています。一方、ArrayBufferArrayのように要素の追加や削除はできませんが、処理速度が速く、また、バイト(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

NEQTOとは

NEQTOハードウェア

NEQTO技術ドキュメント