寒い季節になりましたね。 先月頃までは気分よく外出もできたのですが、最近は寒さに負けて家にこもりっきりの日々です。
となると家での時間が増えてきましたので、久々にちょっとした実験でもやろうかなと思った次第です。
ということで、最近よく耳にするWebAssemblyを使って、ブラウザ上で高度な画像処理ができるようになったのかを実験してみました。
はじめて触る技術ですので、若干の曖昧さがありますが、温かく見守っていただけたらと思います。
WebAssembly(Wasm)とは
WebAssembly(略称: Wasm: ワズム)は、バイナリ形式で構成されている仮想マシン用の命令セットで、ブラウザ上でJavaScriptよりも高速に動作するこを目的に開発された技術です。
主な特徴としては、以下のようなものがあります。
- バイナリフォーマットで記述されており、事前に効率化された状態でブラウザ上で直接実行される
- ほぼネイティブに近い速度で動作
- サポート言語が多様で、C、C++、Rustなどからコンパイル可能
記述方法については、WebAssembly Text Format(WAT)というテキスト形式で命令を書くこともできますが、アセンブリに近い言語になっているので記述がなかなか大変です。
ですので、基本的には他のプログラミング言語のコンパイル先としてWasmを生成します。
対応環境については、2024年時点ではSafari、Chrome、Edge、FireFoxを含む多くのブラウザで動作し、HTML、CSS、JavaScriptに続く4つ目のブラウザ上で実行可能なファイル形式として知られています。 現在の対応状況はこちらから確認できます。
WebAssemblyが必要となった背景
通常、WEBブラウザでアプリケーションを実行するにはJavaScriptが用いられています。 しかし、技術の発達とともにJavaScriptに求められる処理も複雑化してきました。
その解決策として、JavaScriptの処理速度を補完する形で、Wasmが開発されました。 Wasmは、事前にコンパイルされた状態で実行されるため、JavaScriptよりも高速に実行できるようになっています。
この性質を利用し、WasmはJavaScriptよりも高度な画像処理や3D描画などの処理で高速化が期待されています。
他にも、近年のクラウド化の風潮も影響しています。
WasmはC系やJava、Rustなどからコンパイルすることができるため、デスクトップ上で動作することを想定していたアプリケーションを、同じコードでブラウザ上でも動作するように変換することが可能になります。
実際に、有名な画像処理ライブラリであるOpenCVをWasm化した、「OpenCV.js」というライブラリが公開され、ブラウザ上でも高度な画像処理が可能になりました。
このように、ブラウザ上で要求される処理速度に応える形で、JavaScriptよりも「高速」で、バックエンドの処理をフロントエンドでも使えるように「軽量化しブラウザで実行可能」にするWebAssemblyが生まれたのです。
ちょっとした実験
それでは、前述したWebAssemblyの強みを確かめるために、ちょっとした実験をしてみましょう。
WebAssemblyの謳い文句が「高速かつ軽量」ということですので、「処理の負荷」と「実行速度」のどちらのコストも大きい画像処理をさせてみようとおもいます。
実用的な環境を想定して、Reactで構築した画像処理Webアプリを、WebAssemblyと純粋なJavaScriptで実装し比較を行います。
しかし、これではブラウザで画像処理をする前提になってしまいますので、そもそもブラウザでは画像処理を実行しないAPIサーバを使ったパターンも用意しておきます。
よって、合計3パターンの実装でアプローチしてみようと思います。
実験内容と方法
実験内容の詳細
React(Vite)で構築した境界値検出Webアプリを、以下の3つのパターンで実装します。
- APIサーバ (Rust)
- WebAssembly (Rustからコンパイル)
- TypeScript
それぞれの形式で記述した境界値検出プログラムの処理時間で比較を行います。
WebAssemblyを記述する言語は、Wasmへのコンパイルが簡単なRustで記述します。それに合わせる形でAPIサーバもRustで構築し、同じ境界値検出プログラムを使用できるようにします。
条件
- 境界値検出の対象とする画像は、3パターン全て同じ画像を使用します。
- 処理に負荷をかけるため、境界値検出をn回実行します。 実行回数は、50回、100回、500回、1000回としそれぞれの実行回数で比較を行います。
- また、計測する時間は「ボタンを押してから描画までの時間」とします。
境界値検出の方法
ちなみに、境界値検出はSobelフィルタアルゴリズムで行います。
前述したOpenCV.jsを使うことでより高速に実行することは可能ですが、OpenCV.jsは既にWasm化されてしまっているので、今回は使わないで行います。
Sobelフィルタのソースコード (TypeScript)
const sobelX: number[][] = [ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1], ] const sobelY: number[][] = [ [-1, -2, -1], [0, 0, 0], [1, 2, 1], ] const sobelFilter = (imageData) => { const width = imageData.width; const height = imageData.height; const inputData = imageData.data; const grayData = new Uint8ClampedArray(width * height); for (let i = 0; i < data.length; i += 4) { const red = data[i]; const green = data[i + 1]; const blue = data[i + 2]; // ITU-R BT.601規格に基づいて輝度計算 grayData[i / 4] = 0.299 * red + 0.587 * green + 0.114 * blue; } const edgeData = new Uint8ClampedArray(width * height); for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { let gx = 0; let gy = 0; // 3x3カーネルの適用 for (let ky = -1; ky <= 1; ky++) { for (let kx = -1; kx <= 1; kx++) { const pixel = grayData[(y + ky) * width + (x + kx)]; gx += pixel * sobelX[ky + 1][kx + 1]; gy += pixel * sobelY[ky + 1][kx + 1]; } } const magnitude = Math.sqrt(gx * gx + gy * gy) // 三平方の定理で勾配計算 edgeData[y * width + x] = magnitude > 255 ? 255 : magnitude } } const outputData = new Uint8ClampedArray(width * height * 4); // エッジの勾配からRGBカラーを持つ配列に変換 for (let i = 0; i < edgeData.length; i++) { const value = edgeData[i]; outputData[i * 4] = value; // R outputData[i * 4 + 1] = value; // G outputData[i * 4 + 2] = value; // B outputData[i * 4 + 3] = 255; // 透明度 } // 結果を新しいImageDataオブジェクトとして返す return new ImageData(outputData, width, height); };
個人的な予想
個人的な予想は、「TypeScriptは処理が遅い」と聞いたことがあるので、以下のようになるのかな?と予想します。(完全に直感です)
処理速度が速い方から順に、
- WebAssembly
- APIサーバ
- TypeScript
とはいえ、私の予想には明確な根拠がなく、直感的ですので、参考までに我らが大先生のChatGPT先生にも予想をきいてみましょう。
先生の予測がこちら
Wasmがもっとも高速であることは私と同意見ですが、TypeScriptがAPIサーバよりも高速とでましたね。APIサーバの通信コストを重く見ているようです。
さて、実際の結果はどうなのでしょうか。
実験の結果
処理速度の比較
まずは、実行結果から。 下の図は、実行回数と処理にかかった時間で表現しています。
やはり、Wasm(緑線)がもっとも処理時間が短くなりましたね。 次点で、TypeScript(青線)、APIサーバ(赤線)という順番になりました。
私の予想では、APIサーバの方が高速かと思ったのですが、TypeScriptの方が速いですね。 思っていたよりもTypeScriptのパフォーマンスは悪くないのかもしれません。
続いて、処理速度の平均でみてみましょう。
速度で比較すると、よりWasm(緑)の圧倒的な処理速度がわかりますね。TypeScript(青)の約2倍ということで、非常に高パフォーマンスになっていることがわかります。
TypeScriptもWasmには劣りますが、APIサーバ(赤)とは約1.6倍の速度がありますね。 APIサーバは、、、通信部分がネックになったのかもしれません。
ということで、私の予想は外れてしまいましたが、 ChatGPT先生の予想は大正解です。やはり、我らが大先生。今後ともよろしくお願いします。
軽量化の確認
軽量化の比較を行う予定だったですが、今回の実装が悪かったのか、WasmとTypeScriptのどちらも処理中にブラウザが応答しなくなりました。(処理自体は動いてます) 当たり前ですが、APIサーバはそのような問題はありません。処理中でも問題なく操作が可能でした。
重たい処理は、Wasmでも重いままなようですね。 大量データを扱う場合は、おとなしくAPIサーバを使った方が良さそうです。
まとめ
今回は、ブラウザ上でWasmを使った画像処理を行い、TypeScriptに比べてどれほどのパフォーマンスを発揮するかの実験を行いました。
結果としては、処理速度は約2倍になりましたが、軽量化についてはTypeScriptとの違いも判断がつかなかった。という結論に至りました。
個人的にはTypeScriptが思ったよりも高パフォーマンスでびっくりでした。ロジックを書くには向いていないと思っていたのですが、軽量なロジックなら任せてもいいのかもしれないですね。
軽量化については、もう少し調査が必要かもしれませんが、今回の結果からわかるのは、大量の処理が必要であればこれまで通りAPIサーバを使った方がよいと思います。一方で、小規模なロジックであればWasmを使ってブラウザ上で実行することも検討してもよいかな?と思いました。
処理回数も10回程度であれば、ブラウザが固まる時間も0.1秒程度でしたので、ある程度は許容できそうに思います。
以上、結論をまとめると、
- WasmはTypeScriptの処理速度の約2倍
- 高度なロジックについては、大量の処理はAPIサーバ、小規模の処理ならWasmで実装しブラウザ上での動作も検討
となりました。 Wasmを使って実装を考えている方がいましたら、ぜひ参考にしてみてください。
軽量化の部分や複数画像ではどうなのか?など、まだまだ甘い部分はありますが、今回はここまでとします。
最後まで読んでいただきありがとうございました。 次は、軽量化も考慮しつつ、画像処理以外にもいろいろな処理を試してみて、Wasmのうま味を理解できるような実験を行ってみたいですね。(未定ですが)
それでは、また。
おわりに
KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp
career.kentem.jp