Rustで書いたWebAssemblyでフィボナッチを計算したらJSより3倍速かった

  • 90
    いいね
  • 10
    コメント

結果

JavaScript

JavaScript_Fibonacci.PNG

elapsed: ~17 sec

WebAssembly

WebAssembly_Fibonacci.PNG

elapsed: ~5 sec

手順

--- 追記 4/9 ---
最新のChrome/Firefoxに対応しているDockerイメージ作りました。
https://github.com/Foo-x/rust-wasm-incoming
こちらを使用します。
--- 追記 ここまで ---

Dockerのインストール

  1. Docker Toolboxをダウンロード
  2. インストーラを実行
  3. Docker Quickstart Terminalを起動し、$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Rust から WebAssembly へのコンパイル

今回は以下のソースを使用します。
フィボナッチ数列の46番目を計算するのにかかった時間を表示するプログラムです。
(46という数字に深い意味はありません。~10秒のオーダーで適当に決めました)

fibo-wasm.rs
use std::time::SystemTime;

fn main() {
    println!("WebAssembly");
    let start = SystemTime::now();
    println!("fibonacci(46) = {}", fibonacci(46));
    match start.elapsed() {
        Ok(elapsed) => {
            println!("elapsed: {} sec", elapsed.as_secs());
        }
        Err(e) => {
            println!("Error: {:?}", e);
        }
    }
}

fn fibonacci(n:i64) -> i64 {
    if n <= 1 {
        1
    } else {
        fibonacci(n - 1) + fibonacci(n - 2)
    }
}

https://github.com/Foo-x/rust-wasm-incoming のREADMEに従ってビルド・コンパイル。

軽く解説します。

  • docker run ... foo まで
    コンテナの起動
  • rustc
    Rustのコンパイル
  • -O
    最適化 (最初これを入れずにコンパイルした結果、計算時間がJavaScriptより遅くなりました)
  • --target wasm32-unknown-emscripten
    WebAssemblyの出力
  • fibo-wasm.rs
    ソースファイル
  • -o fibo-wasm.html
    出力したWebAssemblyを自動的に読み込んでくれるhtmlの作成

結果の確認

  1. Chromeでchrome://flagsにアクセス後、「試験運用版 WebAssembly」を「有効」にする
    Chrome Canaryを使っている記事がほとんどですが、現在は通常のChromeで大丈夫です。
  2. $ python -m SimpleHTTPServer
  3. http://localhost:8000/fibo-wasm.htmlにアクセス

補足

  • JavaScriptのソースは以下です。(html)
fibo-js.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Fibonacci n=46</title>
  <script>
    function fibonacci(n) {
      if (n <= 1) {
        return 1;
      } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
      }
    }
  </script>
</head>
<body>
  <script>
    console.log("JavaScript");
    let start = Date.now();
    console.log(`fibonacci(46) = ${fibonacci(46)}`);
    console.log(`elapsed: ${(Date.now() - start) / 1000} sec`);
  </script>
</body>
</html>
  • JavaScriptとWebAssemblyで有効数字が異なるなど所々適当です。厳密な比較ではないのでご注意ください。

所感

気づいたことや感じたことを徒然と書きます。

  • 実行速度は速いが、ファイルサイズが大きい
    JavaScriptのファイル (fibo-js.html) が 511Byteだったのに対し、WebAssemblyのファイルは実行に必要な.wasmと.jsの合計が 391.4KByteでした。
    そもそもWebAssemblyの目的の一つに、asm.jsのファイルサイズが大きいので、バイナリにしてファイルサイズを小さくしようというものがあります。(実際、同じソースでasm.jsを作成した場合のサイズは 771.1KByteでした)
    小さくしてこれなので、やみくもに使うものではないな、と思いました。
  • なので、重い処理をしないのであれば導入しないべき
    前項の通りなので、WebAssemblyにしたことによってファイルの転送時間が処理にかかる時間より長くなってしまっては本末転倒です。
    いつ導入すべきか?についてはこちらの1.7.の項がわかりやすいです。
    このページにも書かれていますが、導入したことによって実際にパフォーマンスがよくなったかの確認は必須です。
  • WebAssemblyへのコンパイル時間が長い
    Rust自体のコンパイルはmsecのオーダーですが、そこからWebAssemblyへの時間が長いです。
    今回使用した単純なソースでも、コンパイルに4分ほどかかっています。(Intel(R) Core(TM) i7-6700, 8192MB RAM)
    単体で行う処理の確認はRustのまま行い、結合するときのみWebAssemblyにするなど、開発時には工夫が要りそうです。

--- 追記 3/13 ---
コンパイルが遅いのは初回だけのようです
確かに2回目以降のコンパイルは数秒でコンパイルできることを確認しました。

4/9追記で紹介したDockerイメージはキャッシュを含んでいるので、そのまま使用できます。
--- 追記 ここまで ---

ここまでマイナス面を挙げてきましたが、プラス面も書いておきます。

  • 何といっても速い
    結果を見たときに衝撃でした。
    もちろんJavaScriptとネイティブでは速度に天と地ほどの差があり、当然といえば当然なのですが、実際に確認してみるとその差に驚きました。
    使える場面が限定的とは言え、手段の一つとして考慮するべきものだと思いました。
  • Rustが書けるようになると楽しそう
    Rustは関数型言語で使われる機能を多く取り入れており、なおかつ記述が簡潔です。
    メモリ安全、データ競合安全のため、並列化の問題が起きにくいです。
    安全性もそうですが、コンパイラがとても優秀で、強力な型推論があります。
    WebAssemblyに対応している言語は今のところ主にC/C++とRustです。
    実用的かは置いておいて、DOMの操作もできます。(rust-webplatform)
    Mozillaのサポートもついています。
    将来性しか感じません。

まとめ

WebAssemblyとJavaScriptの速度比較をしてみました。
まだまだ課題はありますが、これからはWeb上でゲームやシミュレーションなどを扱える機会が増えそうです。
また、最近はクロスプラットフォームの開発が普通になりつつありますが、Webでネイティブアプリ並の処理ができるようになると、こちらに流れることも多くなるのではないかなと思います。
今後に注目です。

追記 3/13

WebAssemblyとJavaScriptのパフォーマンスについて

他にも比較している方がいました。
偶然にも同じフィボナッチをベンチマークに使っていますが、こちらのほうがより詳しいです。
元のソースがRustかCかの違いがありますが、やはりWebAssemblyが3倍程度速いようです。
WebAssemblyはマルチスレッドにも対応するらしいので、そちらにも期待です。

asm.jsとWebAssemblyのパフォーマンスについて

https://hacks.mozilla.org/2016/10/webassembly-browser-preview/

追記 4/9

コメントでいただいたいくつかのパターンについても計算しました。
WebAssemblyとJavaScriptのパフォーマンス比較(再帰・ループ)

エラーが起きた場合

  • Dockerのコマンド実行時にAn error occurred trying to connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.24/images/json: open //./pipe/docker_engine: The system cannot find the file specified.
  • RustからWebAssemblyへコンパイルするときにno such file or directory
    • -v `pwd`:/sourceはホストとコンテナ間でファイルを共有するためのオプションですが、Docker Toolbox for Windowsの場合、ホームディレクトリ以下のファイルしか共有できないようです。
    • つまり、C:\Users\foo\以下でのみ実行できます。
    • http://stackoverflow.com/a/34255036

参考

asm.js / WebAssembly

Rust

Docker for Windows