SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Rust言語で作るWebバイナリファイル「WebAssembly」入門

Rustでわかる! WebAssemblyのメリット~処理速度の違いを、サンプルを作りながら体験しよう

Rust言語で作るWebバイナリファイル「WebAssembly」入門 第3回

WebAssemblyとJavaScriptの処理速度の比較

 リスト2と同じ処理をJavaScriptでも実装した図3のサンプル(p002-wasm-vs-js-web)で、WebAssemblyとJavaScriptの処理速度を比較してみます。このサンプルは、指定された計算回数で、WebAssemblyまたはJavaScriptの処理で円周率を計算して、計算結果と処理時間を画面に表示します。JavaScriptの実装は後述する3パターンを用意しました。

図3 WebAssemblyとJavaScriptで円周率を計算するサンプル(p002-wasm-vs-js-web)

 CPU:Intel Core i5-6400、メモリ:16GBのPCを利用して、計算回数10,000,000回で図3のサンプルを実行すると、WebAssemblyとJavaScriptそれぞれの計算時間は表1の通りとなりました。JavaScriptは実装の仕方によりWebAssemblyと同等の速度の場合も、大幅に速度が遅くなる場合もありました。このような速度差が発生する原因を、以下でサンプルの実装内容とともに説明していきます。

表1 WebAssemblyとJavaScriptでの円周率計算時間(p002-wasm-vs-js-web)
計算方法 計算時間(ミリ秒、5回の平均値)
WebAssembly 35.0
JavaScript(JS1ボタン) 165.6
JavaScript(JS2ボタン) 36.8
JavaScript(JS3ボタン) 4453.2

 まず、このサンプルでは、時間のかかる計算処理を「Web Worker」でバックグラウンド処理するため、webpackの「workerize-loader」を利用します。workerize-loaderはリスト7のコマンドでインストールします。「--save-dev」は開発用のツールとしてインストールするオプションです。

[リスト7]プロジェクトにworkerize-loaderをインストールするコマンド
npm install workerize-loader --save-dev

 「WebAssembly」ボタンに対応した実装は、リスト8の通りです。

[リスト8]「WebAssembly」ボタンに対応した実装(p002-wasm-vs-js-web/index.js)
import worker from 'workerize-loader!./worker'; // ...(1)
const instance = worker(); // ...(2)
// WASM ...(3)
document.getElementById('start-wasm').addEventListener('click', async () => {
    document.getElementById('answer').textContent = '計算中...';
    document.getElementById('time').textContent = '測定中...';
    const count = BigInt(document.getElementById('count').value)
    const startTime = Date.now();
    const value = await instance.calcPiUsingWasm(count); // ...(4)
    const endTime = Date.now();
    document.getElementById('answer').textContent = value;
    document.getElementById('time').textContent = endTime - startTime;
});

 (1)でWeb Workerで行う処理を記述したworker.js(後述)をworkerize-loaderで参照して、(2)で処理の実体instanceを生成します。この記述により、instanceのメソッドはWeb Workerで実行されるようになります。「WebAssembly」ボタン押下時のイベント処理(3)で、instance.calcPiUsingWasmメソッド(4)を呼び出して円周率の計算処理を実行します。(4)の前後の記述は、実行時間の測定や画面更新の処理です。

 calcPiUsingWasmメソッドは、worker.jsにリスト9の通り実装します。(1)のexportキーワードは関数が公開されることを、また、asyncキーワードは非同期処理を行うことを表します。(2)でWebAssemblyのパッケージを動的にインポートして、(3)でcalc-piメソッドを実行します。

[リスト9]「WebAssembly」ボタンで実行される計算処理(p002-wasm-vs-js-web/worker.js)
export async function calcPiUsingWasm(count) { // ...(1)
    const { calc_pi } = await import ('p001-calc-pi'); // ...(2)
    return calc_pi(count);// ...(3)
}

 JavaScriptの処理についても、リスト8、9同様に実装します。まず、「JS1」ボタンで実行されるworker.jsの計算処理はリスト10の通りです。Rustの実装(リスト2)をほぼそのままJavaScriptに書き直しています。

[リスト10]「JS1」ボタンで実行される計算処理(p002-wasm-vs-js-web/worker.js)
export function calcPiUsingJS1(count) {
    let i = 1;
    let value = 1.0;
    while (i <= count) { // ...(1)
        const index = 2 * i - 1;
        value = value
            - 1.0 / (2 * index + 1)
            + 1.0 / (2 * (index + 1) + 1);
        i += 1;
    }
    return value * 4.0;
}

 リスト10では、引数countはBigInt、変数iはNumberなので、(1)でBigIntとNumberの比較が発生し、この比較が全体の処理速度に影響します。そこで「JS2」ボタンの処理(リスト11)では、(1)で引数のcountを先にNumberの変数nCountに変換して、以後の処理ではnCountを利用するようにしました。この場合、処理速度はWebAssemblyと同等になりますが、計算精度はNumber(64ビット浮動小数点数)になり、整数部を符号なし64ビットで計算していたRustとは、厳密には計算精度が異なります。

[リスト11]「JS2」ボタンで実行される計算処理(p002-wasm-vs-js-web/worker.js)
export function calcPiUsingJS1(count) {
    const nCount = Number(count); // ...(1)
    let i = 1;
    let value = 1.0;
    while (i <= nCount) {
        const index = 2 * i - 1;
        value = value
            - 1.0 / (2 * index + 1)
            + 1.0 / (2 * (index + 1) + 1);
        i += 1;
    }
    return value * 4.0;
}

 Rustと同じ精度で処理が行えるよう、「JS3」ボタンの処理(リスト12)では、整数部分をすべてBigIntで計算します(「1n」や「2n」のように数値の後ろに「n」をつけるとBigIntになります)。この場合、BigIntの計算が多く発生して、処理速度が低下します。

[リスト12]「JS3」ボタンで実行される計算処理(p002-wasm-vs-js-web/worker.js)
export function calcPiUsingJS1(count) {
    let i = 1n;
    let value = 1.0;
    while (i <= count) {
        const index = 2n * i - 1n;
        value = value
            - 1.0 / Number(2n * index + 1n)
            + 1.0 / Number(2n * (index + 1n) + 1n);
        i += 1n;
    }
    return value * 4.0;
}

 今回実装したサンプルでは、JavaScriptのNumberで扱いきれない大きな数値を利用した大規模な計算を、Rustで実装されたWebAssemblyがJavaScriptより高速に行えました。一般にプログラムの処理速度は実装内容や環境で変わりうるため、WebAssemblyがJavaScriptより常に高速とは限りませんが、今回のサンプルは、WebAssemblyが速度面でより有利になる局面があることを示しています。

次のページ
RustのクレートをWebAssemblyに取り込む

この記事は参考になりましたか?

Rust言語で作るWebバイナリファイル「WebAssembly」入門連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト  吉川 英一(ヨシカワ エイイチ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/15920 2022/06/02 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー