WebAssembly (Wasm)入門
仕組みの理解とJavaScriptとのパフォーマンス比較

2025年4月28日掲載

governance

Zoom、Google Meet、Teamsなどのオンライン会議ツールをWebブラウザ上で利用する機会が増えてきました。背景をぼかしたりリアルタイムの音声文字起こしなどの機能も今や一般的となっています。

しかし、これらの高度な処理がブラウザ上でどのように実現されているか、不思議に思ったことはないでしょうか?実は、これらのリアルタイム処理は、WebAssembly(Wasm)という技術によって実現されていることが多いのです。

本記事では、既存のWasm対応したAIモデルなどを利用するのではなく、C++でゼロからコードを書いて、それをWasmにコンパイルし、実際に動作させるプロセスを通じて、Wasmの仕組みとその効果を理解することを目指します。

目次

WebAssembly (Wasm) とは?

1. 概要

Wasmは、ブラウザ上で高速にプログラムを動作させるためのバイナリフォーマットの一種です。主な特徴は以下の通りです:

  • 高速性: JavaScriptよりも高速に実行できる。
  • 言語非依存: C、C++、Rustなどの言語で書かれたコードをコンパイルして利用可能。

2. ユースケース

JavaScriptは柔軟で使いやすい言語ですが、計算集約的な処理には限界があります。一方、Wasmは低レベルのバイナリ形式で動作するため、コンピュータの性能を最大限に引き出すことができます。特に以下のような利用シーンで威力を発揮します:

  • 大規模な行列演算
  • ゲームエンジンの物理計算
  • 音声・画像処理

実際に速度を比較してみよう!

本記事ではシンプルな「行列加算(Matrix Addition)」を題材に、以下の方法で速度を比較します:

  1. ネイティブ JavaScript
  2. Wasm
  3. Wasm SIMD
  4. Wasm SIMD マルチスレッド

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

ここからは、画像の通りに動くアプリケーションの構築手順を説明していきます。そして、最後に応答時間を確認します。結果だけ見たい場合は、まとめの直前までスキップしてください。

試験の準備

必要なツールのインストール

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);
        }
    }
}

Wasm変換

インストールしたコマンドを使用して、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

実験結果

wasm
実装方法応答時間
ネイティブ JavaScript5.53 ms
Wasm3.70 ms
Wasm SIMD2.84 ms
Wasm SIMD Threads0.98 ms

まとめ

今回の実験から、次のようなことがわかりました:

  • WasmはJavaScriptよりも高速に動作する
  • SIMDを使用することでさらに速度があがる
  • マルチスレッドを活用すると、さらなるパフォーマンス向上が図れる

今回は数倍程度の性能向上でしたが、本格的なAI処理などをさせると、100倍程度まで高速化する可能性があります。

ただし、Wasmの導入には多少の学習コストが必要です。アプリケーションのニーズに応じて、適切な方法を選択しましょう。

Wasmは、Webアプリケーションの可能性を広げる強力なツールです。この記事を通じて、Wasmの魅力を感じていただけたなら幸いです。ぜひ、自分の手で試してみてください!

おすすめの記事

条件に該当するページがございません