フォーム読み込み中
Zoom、Google Meet、Teamsなどのオンライン会議ツールをWebブラウザ上で利用する機会が増えてきました。背景をぼかしたり、リアルタイムの音声文字起こしなどの機能も今や一般的となっています。
しかし、これらの高度な処理がブラウザ上でどのように実現されているか、不思議に思ったことはないでしょうか?実は、これらのリアルタイム処理は、WebAssembly(Wasm)という技術によって実現されていることが多いのです。
本記事では、既存のWasm対応したAIモデルなどを利用するのではなく、C++でゼロからコードを書いて、それをWasmにコンパイルし、実際に動作させるプロセスを通じて、Wasmの仕組みとその効果を理解することを目指します。
Wasmは、ブラウザ上で高速にプログラムを動作させるためのバイナリフォーマットの一種です。主な特徴は以下の通りです:
JavaScriptは柔軟で使いやすい言語ですが、計算集約的な処理には限界があります。一方、Wasmは低レベルのバイナリ形式で動作するため、コンピュータの性能を最大限に引き出すことができます。特に以下のような利用シーンで威力を発揮します:
本記事ではシンプルな「行列加算(Matrix Addition)」を題材に、以下の方法で速度を比較します:
SIMDやマルチスレッドはWasmのコア仕様ではなく、追加の拡張仕様として提供されています。ブラウザによってサポート状況が異なる場合がある点について注意ください。
WasmのSIMDでは、例えば、v128型(128ビット)型が用意されています。8ビット表現のデータであれば16個同時に処理できます。
マルチスレッドに関しては、JavaScriptでもWeb Workerなどで類似のことができるのでは、と思われた方もいるかもしれません。Web Workerの場合はオブジェクトのコピーによるデータ交換となるため高速処理は不向きです。WasmではShared Memoryを使うため、JavaScriptのWeb Workerよりもマルチスレッドが高速に動作します。
また、Wasmでは直接DOMの操作ができない点に注意が必要です。JavaScriptを経由しないとDOM処理を行えません。
さて、完成予定のWebアプリをお見せします。ボタンを押すと応答時間が出てきます。
どういった結果になるのでしょうか。
ここからは、画像の通りに動くアプリケーションの構築手順を説明していきます。そして、最後に応答時間を確認します。結果だけ見たい場合は、まとめの直前までスキップしてください。
Wasmへの変換にはEmscriptenを使用します。Emscriptenで、C++のソースコードをコンパイルし、WebブラウザやJavaScript環境で利用可能なWasmモジュールと、対応するJavaScriptラッパーを生成します。
Emscriptenコンパイラとその関連ツールを管理するためのSDK(Software Development Kit)である、emsdkをインストールします。
下記のコマンドでインストールできます。
C++のコードを書き終わった後に利用します。
$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest
$ source emsdk/emsdk_env.sh
1. ネイティブ JavaScript
まずは、ネイティブなJavaScript部分です。matrixAddNative関数だけを見てください。JavaScriptだけで処理を書いています。
また比較用の2、3、4の処理も、ボタンを押したら反応するように、本HTML内でしかけています。
(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance Comparison</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
button {
margin: 5px;
padding: 10px 20px;
font-size: 16px;
}
#results {
margin-top: 20px;
font-size: 18px;
}
</style>
</head>
<body>
<h1>Performance Comparison: CPU vs Wasm vs Wasm SIMD vs Wasm SIMD Threads</h1>
<button id="run-native">Run Native (JavaScript)</button>
<button id="run-wasm">Run Wasm</button>
<button id="run-wasm-simd">Run Wasm SIMD</button>
<button id="run-wasm-simd-threads">Run Wasm SIMD Threads</button>
<div id="results"></div>
<script src="matrix_add.js"></script>
<script src="matrix_add_simd.js"></script>
<script src="matrix_add_simd_threads.js"></script>
<script>
const SIZE = 1024;
const A = new Float32Array(SIZE * SIZE).fill(1.0);
const B = new Float32Array(SIZE * SIZE).fill(2.0);
const C = new Float32Array(SIZE * SIZE);
let wasmModule, wasmModuleSIMD, wasmModuleSIMDThreads;
Promise.all([
MatrixAdd().then(instance => { wasmModule = instance; }),
MatrixAddSIMD().then(instance => { wasmModuleSIMD = instance; }),
MatrixSIMDThreads().then(instance => { wasmModuleSIMDThreads = instance; })
]).then(() => {
console.log("All modules loaded successfully.");
});
function measureTime(fn, label) {
const start = performance.now();
fn();
const end = performance.now();
const time = (end - start).toFixed(2);
document.getElementById('results').innerHTML += `<p>${label}: ${time} ms</p>`;
}
function matrixAddNative(A, B, C, size) {
for (let i = 0; i < size * size; ++i) {
C[i] = A[i] + B[i];
}
}
document.getElementById('run-native').addEventListener('click', () => {
measureTime(() => matrixAddNative(A, B, C, SIZE), 'Native JavaScript');
});
document.getElementById('run-wasm').addEventListener('click', async () => {
if (!wasmModule) {
alert("Wasm module is not loaded yet.");
return;
}
await wasmModule.onRuntimeInitialized;
measureTime(() => wasmModule._matrixAdd(A.byteOffset, B.byteOffset, C.byteOffset, SIZE), 'Wasm');
});
document.getElementById('run-wasm-simd').addEventListener('click', async () => {
if (!wasmModuleSIMD) {
alert("Wasm SIMD module is not loaded yet.");
return;
}
await wasmModuleSIMD.onRuntimeInitialized;
measureTime(() => wasmModuleSIMD._matrixAddSIMD(A.byteOffset, B.byteOffset, C.byteOffset, SIZE), 'Wasm SIMD');
});
document.getElementById('run-wasm-simd-threads').addEventListener('click', async () => {
if (!wasmModuleSIMDThreads) {
alert("Wasm SIMD Threads module is not loaded yet.");
return;
}
await wasmModuleSIMDThreads.onRuntimeInitialized;
measureTime(() => wasmModuleSIMDThreads._matrixAddSIMDThreads(A.byteOffset, B.byteOffset, C.byteOffset, SIZE), 'Wasm SIMD Threads');
});
</script>
</body>
</html>
2. Wasm
C++で行列加算する処理を素直に書いています。
(matrix_add.cpp)
#include <emscripten.h>
#include <vector>
extern "C" {
EMSCRIPTEN_KEEPALIVE
void matrixAdd(const float* A, const float* B, float* C, int size) {
for (int i = 0; i < size * size; ++i) {
C[i] = A[i] + B[i];
}
}
}
3. Wasm SIMD
SIMD対応させたC++コードとなります。
(matrix_add_simd.cpp)
#include <emscripten.h>
#include <wasm_simd128.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
void matrixAddSIMD(const float* A, const float* B, float* C, int size) {
for (int i = 0; i < size * size; i += 4) {
v128_t a = wasm_v128_load(&A[i]);
v128_t b = wasm_v128_load(&B[i]);
v128_t c = wasm_f32x4_add(a, b);
wasm_v128_store(&C[i], c);
}
}
}
4. Wasm SIMD マルチスレッド
3のコードと同じことをスレッド対応させています。
(matrix_add_simd_threads.cpp)
#include <emscripten.h>
#include <emscripten/threading.h>
#include <wasm_simd128.h>
#include <pthread.h>
#include <vector>
struct ThreadParams {
const float* A;
const float* B;
float* C;
int start;
int end;
};
void threadFunction(const float* A, const float* B, float* C, int start, int end) {
for (int i = start; i < end; i += 4) {
v128_t a = wasm_v128_load(&A[i]);
v128_t b = wasm_v128_load(&B[i]);
v128_t c = wasm_f32x4_add(a, b);
wasm_v128_store(&C[i], c);
}
}
void* threadWrapper(void* arg) {
ThreadParams* params = reinterpret_cast<ThreadParams*>(arg);
threadFunction(params->A, params->B, params->C, params->start, params->end);
delete params; // メモリリークを防ぐ
return nullptr;
}
extern "C" {
EMSCRIPTEN_KEEPALIVE
void matrixAddSIMDThreads(const float* A, const float* B, float* C, int size) {
int numThreads = emscripten_num_logical_cores();
int totalElements = size * size;
int chunkSize = totalElements / numThreads;
int remainder = totalElements % numThreads; // 余りを処理
std::vector<pthread_t> threads(numThreads);
for (int i = 0; i < numThreads; ++i) {
int start = i * chunkSize;
int end = start + chunkSize;
if (i == numThreads - 1) {
end += remainder; // 最後のスレッドが余りを処理
}
ThreadParams* params = new ThreadParams{A, B, C, start, end};
if (pthread_create(&threads[i], NULL, threadWrapper, params) != 0) {
delete params; // pthread_create失敗時にリークを防ぐ
printf("Error: Failed to create thread %d\n", i);
}
}
for (pthread_t& thread : threads) {
pthread_join(thread, NULL);
}
}
}
インストールしたコマンドを使用して、C++コード(.cpp)を Wasm(.wasm)とJavaScriptラッパー(.js)にコンパイルします。
このとき、生成された.jsファイルは、WebAssemblyモジュールの読み込み・初期化するためのヘルパー関数を提供します。
生成された.jsファイルをHTML内のscriptで読み込みます。その.jsファイルには、.wasmモジュールを読み込むコードも含まれています。.wasmファイルは、セキュリティ制約のため、直接読み込むことはできないためです。
$ emcc matrix_add.cpp \
-O3 \
-s Wasm=1 \
-s MODULARIZE \
-s EXPORT_NAME="MatrixAdd" \
-o matrix_add.js
$ emcc matrix_add_simd.cpp \
-O3 \
-msimd128 \
-s Wasm=1 \
-s MODULARIZE \
-s EXPORT_NAME="MatrixAddSIMD" \
-o matrix_add_simd.js
$ emcc matrix_add_simd_threads.cpp \
-O3 \
-msimd128 \
-s Wasm=1 \
-s MODULARIZE \
-s EXPORT_NAME="MatrixSIMDThreads" \
-s USE_PTHREADS=1 \
-s PTHREAD_POOL_SIZE=4 \
-o matrix_add_simd_threads.js
先ほど作成したHTMLファイルに合わせて、コンパイルされた.jsファイルと.wasmファイルはindex.htmlと同じディレクトリに配置します。
あとはWebサーバを立てれば終わりです。簡易的にサーバを立ち上げてしまいましょう。
$ ruby -run -e httpd . --bind-address 0.0.0.0 --port=3000
http://localhost:3000/index.html
と言いたいのですが、アプリへアクセスできても、マルチスレッド版がエラーが出てボタンが反応しないはずです。
ブラウザの制限により、SharedArrayBufferを使うためのレスポンスヘッダの設定と、HTTPS化が必要となります。
SSL証明書を用意して、rackから操作しましょう。
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
config.ru
require 'webrick'
require 'webrick/https'
require 'openssl'
root = Dir.pwd
begin
certificate = OpenSSL::X509::Certificate.new(File.read('cert.pem'))
private_key = OpenSSL::PKey::RSA.new(File.read('key.pem'))
rescue Errno::ENOENT
puts "Error: Certificate or key file not found"
exit 1
end
server = WEBrick::HTTPServer.new(
BindAddress: "0.0.0.0",
Port: 443,
DocumentRoot: root,
SSLEnable: true,
SSLCertificate: certificate,
SSLPrivateKey: private_key,
SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE
)
server.mount_proc '/' do |req, res|
path = File.join(root, req.path_info)
if File.exist?(path) && !File.directory?(path)
res.body = File.read(path)
res['Content-Type'] = WEBrick::HTTPUtils.mime_type(path, WEBrick::HTTPUtils::DefaultMimeTypes)
# SharedArrayBuffer を有効にするためのヘッダ
res['Cross-Origin-Opener-Policy'] = 'same-origin'
res['Cross-Origin-Embedder-Policy'] = 'require-corp'
else
res.status = 404
res.body = "File not found: #{req.path_info}"
end
end
trap 'INT' do
server.shutdown
end
server.start
これでエラーは解消されるでしょう。
起動します。
$ sudo ~/.rbenv/shims/ruby config.ru
アクセスして、それぞれのボタンを押してパフォーマンスを測定してみてください。
https://127.0.0.1/index.html
| 実装方法 | 応答時間 |
|---|---|
| ネイティブ JavaScript | 5.53 ms |
| Wasm | 3.70 ms |
| Wasm SIMD | 2.84 ms |
| Wasm SIMD Threads | 0.98 ms |
今回の実験から、次のようなことがわかりました:
今回は数倍程度の性能向上でしたが、本格的なAI処理などをさせると、100倍程度まで高速化する可能性があります。
ただし、Wasmの導入には多少の学習コストが必要です。アプリケーションのニーズに応じて、適切な方法を選択しましょう。
Wasmは、Webアプリケーションの可能性を広げる強力なツールです。この記事を通じて、Wasmの魅力を感じていただけたなら幸いです。ぜひ、自分の手で試してみてください!
条件に該当するページがございません