【JavaScript】Number型の配列をFloat32Arrayへ変換して、バイナリのままbase64で保存【データ圧縮】

#はじめに
乱数をn個発生させ、それを配列で保持します。
その配列を文字列することに興味があるとします。
そのままJSON文字列にすると、配列のサイズにもよりますが、中々のサイズになります。
もし、多少精度が落ちてもいいのなら、まあまあ圧縮できます。
Number型は、8バイトですので、Float32Arrayに変換するだけでも、サイズが半分になります。
4バイトでも十分なケースって結構多いんじゃないでしょうか。
そのFloat32Arrayをバイナリのまま、base64にします。
要は、以下の2つのパターンで比較をしたいってことです。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/b5f97c42-a4e2-0d41-2adf-f669df6b5376.png)

#圧縮率の理論値
圧縮率を「削減された量の元の量に対する割合」とします。
ざっくり計算してみます。
そのままjson文字列にすると、今回発生させる乱数は0から1なので、
0と小数点とカンマで3バイト、有効桁数が16桁、合計で19文字ですので、
約19*nバイトになります。
一方、Float32Arrayをバイナリのままbase64にした場合、
Float32Arrayは4バイトで、base64にする時に、約4/3倍になりますので
合計で約(4 * 4 / 3)*nバイトとなります。
圧縮率は16/3÷19= 0.28070175438=28%
となり、まあまあ圧縮できます。
#プログラムで確認(ログ出力)
Number型配列→JSON文字列→Number型配列・・・①
Number型配列→Float32Array→base64→Float32Array→Number型配列・・・②
①と②を比較すると、大体以下のようになります。
文字列のサイズ ①：②=100:28
Number型配列から文字列にするまでの処理時間 ①：②=1:2
文字列からNumber型配列にするまでの時間 ①：②=5:1
全体の処理時間 ①：②=1:2

###乱数を100000個発生させた場合のログ
![100000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/a5fc0f44-f218-592d-23e8-e258693cf1bb.png)
###乱数を500000個発生させた場合のログ
![500000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/74d1af53-cefd-cd00-fe4c-982225af21f0.png)
###乱数を1000000個発生させた場合のログ
![1000000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/43026b49-dd21-c330-a336-28f4b8af63b3.png)


##Float32Array→base64が遅い
Float32Arrayをbase64にするところですが、
配列の長さが大きくなると発生するmaximum call stack size exceededを避けるために
reduceでループ処理しているのですが、それが遅いのかなーって思っています。
<a href="https://stackoverflow.com/questions/56454596/maximum-call-stack-size-exceeded-on-converting-file">こちら</a>を参考にして作成しました。

よい方法があれば教えてください。
マルチスレッドは使えそうですねー。

#ソース
※プログラムの動作確認は、PC版のChromeで行いました。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>float32array sample</title>
<script>
let startTime, endTime;

// 乱数の配列を作成する
const array = [];
for(let i = 0, len = 500000; i < len; i += 1) {
    array.push(Math.random());
}

// Number型配列をJSON文字列に変換
startTime = new Date();
const json = JSON.stringify(array);
endTime = new Date();
const arrayToJsonTime = endTime - startTime;

// JSON文字列をNumber型配列に変換
startTime = new Date();
const parsedArray = JSON.parse(json);
endTime = new Date();
const jsonToArrayTime = endTime - startTime;

// 配列をFloat32Arrayへ変換
startTime = new Date();
const float32Array = arrayToFloat32Array(array);
endTime = new Date();
const arrayToFloat32ArrayTime = endTime - startTime;

// Float32Arrayをbase64へ変換
startTime = new Date();
const base64 = float32ArrayToBase64(float32Array);
endTime = new Date();
const float32ArrayToBase64Time = endTime - startTime;

// base64をFloat32Arrayへ変換
startTime = new Date();
const loadedFloat32Array = base64ToFloat32Array(base64);
endTime = new Date();
const base64ToFloat32ArrayTime = endTime - startTime;

startTime = new Date();
const loadedArray = float32ArrayToArray(loadedFloat32Array);
endTime = new Date();
const float32ArrayToArrayTime = endTime - startTime;

console.table([
	{ key: '乱数の数', value: array.length },
	{ key: 'Number型配列の使用メモリ(byte)', value: array.length * 8 },
	{ key: 'JSON文字列のサイズ(byte)', value: json.length },
	{ key: 'base64のサイズ(byte)', value: base64.length },
	{ key: '圧縮率(%)', value: 100 * base64.length / json.length },
	{ key: '------------------', value: '---------------------------' },
	{ key: 'Number型配列 -> JSON文字列(ms)', value: arrayToJsonTime },
	{ key: 'JSON文字列 -> Number型配列(ms)', value: jsonToArrayTime },
	{ key: '[合算] Number型配列 -> JSON文字列 -> Number型配列(ms)', value: arrayToJsonTime + jsonToArrayTime },
	{ key: '------------------', value: '---------------------------' },
	{ key: 'Number型配列 -> Float32Array(ms)', value: arrayToFloat32ArrayTime },
	{ key: 'Float32Array -> base64(ms)', value: float32ArrayToBase64Time },
	{ key: 'base64 -> Float32Array(ms)', value: base64ToFloat32ArrayTime },
	{ key: 'Float32Array -> Number型配列(ms)', value: float32ArrayToArrayTime },
	{ key: '[合算] Number型配列 -> Float32Array -> base64 -> Float32Array -> Number型配列(ms)', value: arrayToFloat32ArrayTime + float32ArrayToBase64Time + base64ToFloat32ArrayTime + float32ArrayToArrayTime },
]);

function arrayToFloat32Array(array) {
	const f32 = new Float32Array(array.length);
	array.forEach(function(elm, i) {
	    f32[i] = elm;
	});
	return f32;
}

function float32ArrayToArray(f32) {
	const array = [];
	for(let i = 0, len = f32.length; i < len; i += 1) {
		array.push(f32[i]);
	};
	return array;
}

function float32ArrayToBase64(f32) {
    var uint8 = new Uint8Array(f32.buffer);
    return btoa(uint8.reduce(function(data, byte) {
        return data + String.fromCharCode(byte);
    }, ''));
}

function base64ToFloat32Array(base64) {
    var binary = atob(base64),
        len = binary.length,
        bytes = new Uint8Array(len),
        i;
    for(i = 0; i < len; i += 1) {
        bytes[i] = binary.charCodeAt(i);
    }
    return new Float32Array(bytes.buffer);
}
</script>
</head>
<body>
</body>
</html>
```

#まとめ
Number型の配列を少し精度落としてもよい(double → float)なら、
それを文字列にするとき、圧縮率28%が実現できる。
処理時間は高々2倍である。
結構有用じゃないかなって思っています。:relaxed: