Rust(wasm)のimage::load_from_memory遅すぎ問題
load_from_memoryを迂回すればなんとかなる
関連記事
こんな記事がある
(^_^;)煽ってんなぁ。。。
VanillaJS(素のJSのこと), WASM, OpenCVで画像のサイズ変更処理速度を比較している内容。
WebAssemblyが圧倒的に遅いという結果になっている。
(・ω・) ほんまかいな
Qiita記事のコメント欄にあるように
JSからCanvasAPI(ネイティブ)を直接叩いて変換しているわけで
かなりアンフェアな比較記事だ。
(・∀・;) そりゃ遅いよね
ということで自分で検証してみることにした。
ボトルネックはこれ
image::load_from_memory
もう絶望的に遅い。
元記事でも一番小さい画像のロードだけで4秒かかってる。
元記事で使ってる画像サイズがでかすぎる問題もあるが、
速度は速いに超したことはない。
一ヶ月ほど調査、試行錯誤してみたが、
ミリ秒オーダーになるような劇的に良くなる方法というのは見つからなかった。
ただ、ロード時間は1秒オーダーぐらいにはなった。
問題点を整理しながら、
実装してみよう。
ヾ(・ω<)ノ" 三三三● ⅱⅲ コロコロ♪
------------------- ↓ 本題はここから ↓-------------------
再実装ソースコード
ソースを公開しているので、
実際にビルドして確認してもらえば良いと思う。
git clone https://github.com/dohzoh/wasm-image-resizer.git
cd wasm-image-resizer/sample
npm i
cd ..
sh ./build.sh
sh ./serve.sh
元記事の問題点を整理
問題点1: 画像がでかすぎる
元記事の画像サイズは以下の通り
name | size | value |
---|---|---|
JohnDayRiver.html.jpg | 5184x3456 | 7.0 MB |
LakeMcDonald.html.jpg | 6016x4000 | 14.7 MB |
LoneStarGeyser.html.jpg | 5760x3840 | 17.4 MB |
比較記事のためなんだと思うが、
でかすぎてあまりにも現実的でない。
(このぐらい使うこともあるのかな、ちょっとわからん。)
一応本記事ではそのまま扱うが、
画像がでかいことは頭の中に入れておいて欲しい。
問題点2: 実装方法がおかしい
比較記事としての問題点に実装方法の違いがある
name | detail |
---|---|
JS(Native) | img fetch -> canvas load(on html) -> CanvasAPI resize -> result |
OpenCV | img fetch -> canvas load(on html) -> resize(on OpenCV) -> result |
WASM | img fetch -> toArray -> wasm binary load -> image load(on wasm) -> resize(on wasm) -> toBlob -> result |
WebAssemblyだけやたら行程が多い。
アンフェアな比較と言われるのも納得。
(細かく言えばOpenCV側も問題あるがそれは割愛)
なのでWebAssembly側も行程を揃える。
問題点3: 入力値が異なる
実装方法と内容が被るが、
比較記事にするなら入力値は揃えるのは当たり前だ。
だが、JS, OpenCVともにCanvasオブジェクトなのに、
wasmだけblobってのはどういうことだろうか。
なので入力をCanvasオブジェクトにする。
問題点4: image::load_from_memory遅すぎ問題
そして一番の問題はimage::load_from_memoryだ。
自身でも試したがとにかく遅い。
本記事ではこれを使わずに実装する。
実測
では元の状態でWebassemblyの実測を行う。
load_from_memoryに絞って計測
一番サイズの大きいLoneStarGeyser.html.jpg
を使用。
再実装後の結果
結果からご覧いただこう
1/4ぐらいの時間にはなっている。
JSやOpenCVに比べれば全然遅いが、
非現実的な画像サイズで一秒そこらだったら実装の見せ方でどうにでもなるので、
実用レベルではあると思っている。
詳しくは公開しているソースを見てもらえば良いと思うが、
ここでは要点を掻い摘まんで説明する。
image::load_from_memoryの入出力
load_from_memoryを置き換えるのであれば、
これの動きをトレースして別の方法を考える必要がある
image::load_from_memoryの仕様を見ると
pub fn load_from_memory(buffer: &[u8]) -> ImageResult<DynamicImage>
[u8]とはUint8Arrayのこと思って良いので、
Rust的にはjs-sys::Uint8Array => image::DynamicImageになれば良いと言うことになる。
そして、冒頭のようにcanvasを入力値にするということを踏まえると
以下のような流れになる。
canvas -> ctx.get_image_data -> ImageData -> ImageBuffer -> DynamicImage
実装ベースでみる
上記説明だと何が何やらなので、
コードに落とし込むと。
use image::*;
use js_sys::*;
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
・・・
// canvas: HtmlCanvasElement,
// ctx: CanvasRenderingContext2d,
let image_width = canvas.width();
let image_height = canvas.height();
// ctx.getImageData -> ImageData
let data: ImageData = ctx
.get_image_data(0.0, 0.0, image_width as f64, image_height as f64)
.unwrap();
// imageData -> Uint8ClampedArray
let image_data = data.data().to_vec();
// Uint8ClampedArray -> ImageBuffer
let img_buffer = ImageBuffer::from_vec(image_width, image_height, image_data).unwrap();
// ImageBuffer -> DynamicImage
let dynamic_image = DynamicImage::ImageRgba8(img_buffer);
あとはimage::DynamicImageの機能で、
リサイズと出力をしている。
再実装ソースコード
実装したソースは以下に記載しておく。
もともとsvelteで遊ぶ題材を探していて当該記事に当たったので、
良い題材にはなった。
(苦労はしただけで、報われもしてないけど。)
rollup-2ブランチにはそれらを踏まえてsvelteで遊んだものがある。
ご興味、お時間あればどうぞ。
git clone -b rollup-2 https://github.com/dohzoh/wasm-image-resizer.git
cd wasm-image-resizer
npm i
npm run wasm
npm run dev
------------------- ↓ 後書きはここから ↓-------------------
以前にも書いたが、
WebAssembly実装は「かなり気をつけて」行う必要がある。
特にJS<->WASM間のデータのやりとりは可能な限り小さくしなければならない。
(このサンプルではwasmからconsole.logを読んでいるがこれもダメな実装)
リサイズも高速化
元記事で使用しているimage::resize_exactも速度的に見れば遅い。
なので、image::imageops::resizeを使う。
let resized = DynamicImage::ImageRgba8(image::imageops::resize(
&dynamic_image,
width as u32,
height as u32,
imageops::FilterType::Nearest,
));
もとが940msなのでだいたい1/3ぐらいになっている。
SIMD命令を使うやつも考えたが、
使い方が難しくてよくわからないのと、
Wasmで動くかもわからないので止めた。
photon
さて、上記ソースだが、
私がゼロから書いたわけではなく、
ほぼほぼphotonのパクりだ。
photonはimageクレートを高速化することを目的としたクレート。
ただ、これをそのまま使うとwasmデータが肥大化するので、
参考にしているにとどめている。
Discussion