canvas ピクセルデータを hex 文字列に変換する

## ことの発端

html の canvas 要素からピクセルデータを取得して、hex文字列にしなきゃいけない場面があり、こんなコードを書いてみた。

```ts
function toString(image: ImageData){
  const hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] as readonly string [];
  return image.data.map(x => hex[x >> 4 & 0xF] + hex[x & 0xF]).join("");
}
```

そうすると、こんな怒られ方をする。

```
Argument of type '(x: number) => string' is not assignable to parameter of type '(value: number, index: number, array: Uint8ClampedArray) => number'.
  Type 'string' is not assignable to type 'number'.
```

う〜〜〜ん、Uint8ClampedArray の map() って、ちょっと変わった子なのねと分かったものの、さて、どうしたものか。。。
あ、兄弟の Uint8Array なんかも同様ですね。

## forEach で回しながら文字列を作成する

ベタな方法です。
string の "+=" がどんだけ頭が良いかで、パフォーマンスに影響出そうです。

```ts
function toString(image: ImageData){
  const hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] as readonly string [];
  let s = "";
  image.data.forEach((x) => {
    s += hex[x >> 4 & 0xF] + hex[x & 0xF];
  });
  return s;
}
```


## Array.from() で number[] に変換してから map() と join() でやっつける

お〜、何だかシンプル。
ただ、Array.from() って、メモリコピーが発生するか否かで、かなりパフォーマンスに影響出るんじゃないかな？

```ts
function toString(image: ImageData){
  const hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] as readonly string [];
  return Array.from(image.data).map(x => hex[x >> 4 & 0xF] + hex[x & 0xF]).join("");
}
```

## 実際に速度計測してみる

こういう時は、実際に速度計測するのが間違いないってことで、こんなコードを書いてみる。
ここでは、Uint8ClampedArrayじゃなくて、

```ts
const hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] as readonly string [];

const arr: number[] = [];

/* Array のサンプルデータを作成 */
for(let i = 0; i < 0x1000; i++) arr[i] = i & 0xFF;

/* Uint8ClampedArray のサンプルデータを作成 */
const u8arr = Uint8ClampedArray.from(arr);

/* 結果を入れる器を初期化 */
const re_1: number[] = [];
const re_2: number[] = [];
const re_3: number[] = [];
const re_4: number[] = [];


for(let i = 0; i < 100; i++){
    /* (1) Array を map で変換しつつ join */
    {
        const t0 = performance.now();
        const s = arr.map(x => hex[x >> 4 & 0xF] + hex[x & 0xF]).join("");
        const t1 = performance.now();
        //console.log(`time: ${t1 - t0}ms`);
        re_1.push(t1 - t0);
        console.log(s.length);
    }

    /* (2) Array を forEachで回しつつ文字列を追加 */
    {
        const t0 = performance.now();
        const s = (() => {
            let _s = "";
            arr.forEach((x) => {
                _s += hex[x >> 4 & 0xF] + hex[x & 0xF];
            });
            return _s;
        })();
        const t1 = performance.now();
        //console.log(`time: ${t1 - t0}ms`);
        re_2.push(t1 - t0);
        console.log(s.length);
    }

    /* (3) Uint8ClampedArray を forEachで回しつつ文字列を追加 */
    {
        const t0 = performance.now();
        const s = (() => {
            let _s = "";
            u8arr.forEach((x) => {
                _s += hex[x >> 4 & 0xF] + hex[x & 0xF];
            });
            return _s;
        })();
        const t1 = performance.now();
        //console.log(`time: ${t1 - t0}ms`);
        re_3.push(t1 - t0);
        console.log(s.length);
    }


    /* (4) Uint8ClampedArray Array.from で Array にしてから、map で変換しつつ join */
    {
        const t0 = performance.now();
        const s = Array.from(u8arr).map(x => hex[x >> 4 & 0xF] + hex[x & 0xF]).join("");
        const t1 = performance.now();
        //console.log(`time: ${t1 - t0}ms`);
        re_4.push(t1 - t0);
        console.log(s.length);
    }
}

/* 初回はキャッシュの影響とかあるかな〜と思い、結果を捨てる */
re_1.shift();
re_2.shift();
re_3.shift();
re_4.shift();

/* 平均を表示 */
console.log(`(1) ${re_1.reduce((p, c) => p + c) / re_1.length}ms`);
console.log(`(2) ${re_2.reduce((p, c) => p + c) / re_2.length}ms`);
console.log(`(3) ${re_3.reduce((p, c) => p + c) / re_3.length}ms`);
console.log(`(4) ${re_4.reduce((p, c) => p + c) / re_4.length}ms`);
```

そして、結果は。。。

```
(1) 0.2080303030849947ms
(2) 0.1918181818965799ms 
(3) 0.26181818211258323ms 
(4) 0.32621212136805894ms
```

ブラウザのメモリ使用状況とか、他のプロセスも影響があると思うけど、概ね順位はこうなります。

１位：　Array を forEachで回しつつ文字列を追加
２位：　Array を map で変換しつつ join
３位：　Uint8ClampedArray を forEachで回しつつ文字列を追加
４位：　Uint8ClampedArray Array.from で Array にしてから、map で変換しつつ join

## 結論

map() と join() って、 map() で新たな配列をメモリ上に生成して、join() で更にメモリ上に文字列を生成するという、パフォーマンスに大きく影響するんだなぁ〜と実感。関数型プログラミングも使いどころによっては、パフォーマンスに悪影響が出るという感じかな。
（reactive extensionで from() -> map() -> zip() で繋げればパフォーマンス出そうな予感がするが試してない）
そして、Array.from はメモリアクセスの最適化は行われていないので、やっぱりコピーするんですね。（多分）
つまり、Array.from() で楽しようなんて考えちゃいけません。
（パフォーマンスを求めないのであれば可読性を考えてアリだけどね）

ということで、頭悪そうなコードだけど、Uint8ClampedArray を forEach() でぶん回す方法を採用。