LoginSignup
0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-15

ことの発端

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

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 の "+=" がどんだけ頭が良いかで、パフォーマンスに影響出そうです。

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() って、メモリコピーが発生するか否かで、かなりパフォーマンスに影響出るんじゃないかな?

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じゃなくて、

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

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

1位: Array を forEachで回しつつ文字列を追加
2位: Array を map で変換しつつ join
3位: Uint8ClampedArray を forEachで回しつつ文字列を追加
4位: Uint8ClampedArray Array.from で Array にしてから、map で変換しつつ join

結論

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0