Web Assemblyすべきか否か

いつもよりもぐっとテクニカルな話。

プログラミングや開発案件には色々ありますが、その中でもwebに関わるもののマーケットはいうまでもなく大きく、みんなパソコンでもモバイルでもブラウザを使って日夜web 閲覧に励まれていると思われます。(もしかしたら、若い人はもうブラウザほとんど使わないのかもしれませんが)。

そのブラウザで走る言語といえばJavascriptで、現代においてちょっとした計算をブラウザの中でやろうとしたら、これ一択でしょう。ブラウザに充てられる莫大な投資によって、そのJavascriptなる言語を走らせるインタープリタの性能はは年々上がり数年前に比べて目覚ましい進歩を遂げています。とはいえ、所詮インタープリタ言語であり、c++や今回使うrustのそれと比べると遅いという印象は一般に言えると思います。

そこで近年、もっと機械に近いコードを直接Javascriptのエンジンに渡し、さらにスピードをあげようという試みが注目されています。その名もWebAssembly(通称wasm)。基本的にはc++や(今回使うrust)といった相対的に低級な言語を使って開発し、そこからwasm 互換なバイナリをコンパイルし、それをjavascriptから呼び出すというプロセスになります。

今回はそれを少し試す機会があったのでその紹介です。目的はJSONオブジェクトのハッシュ値(sha256)を求め、conole.log()する簡単なものです。

https://github.com/yasushisakai/json-hasher-wasm

流れとしては、任意のオブジェクトをシリアライズして、得られた文字列をハッシュする事です。特に説明することもないのですが、ハッシュを計算する以外に、一つだけ気に止める事は、オブジェクトだった場合(key,valueペアのデータ)にそのキーの順番をソートする必要がある事です。これによって、入力のオブジェクトを揃えることができるので安心してハッシュできます。こんな感じですか。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// javascript implementation
// stringify in order
function ordered_serialization (object) {
    if(isObject(object)){
        // convert it to an array
        let obj_arr = Object.keys(object).sort();
        let result_string = '{';
        obj_arr.forEach((k,i)=>{
            if(i != 0){
                result_string += ',';
            }
            temp = ordered_serialization(object[k]);
            result_string += `"${k}":${temp}`;
        });
        result_string += '}';
        return result_string;
    } else if (isArray(object)) {
        result_string = '[';
        object.forEach((v,i) => {
            if(i != 0){
                result_string += ',';
            }
            result_string += ordered_serialization(v);
        });
        result_string = ']';
        return result_string;
    } else {
        return JSON.stringify(object);
    }
}

repoに全貌のコードがなぜか見えないのですが、正直全然最適化せずに、僕がいかに Javascriptが嫌いかを体現しています。ちゃんとやるなら、Typescriptで書いて、最適化するんでしょうが、時間がなくてばっとやるという想定です。おまけに肝心のsha256も npmで適当に探した人気のハッシュライブラリなので、そこも時短しております。

さて、Rust版です。Rustはまだまだマイナーだけど開発者の中で4年連続でももっとも愛されている言語で、鬼畜コンパイラーによって鍛えられる(?)新しい言語です。Rustの変態性についてはいつかにとっておいて、上のJSのコードをRustで書くとこんな感じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#[wasm_bindgen]
pub fn hash_json(js_value: &JsValue) -> String {
    let json: Value = js_value.into_serde().unwrap();
    let buf = json_to_string(&json).unwrap();
    format_hash(&hash(&buf))
}

/// json objects has arbitrary order. This sorts the map
/// key's in alphabetical order to be sure the output is consistent
fn json_to_string(json: &Value) -> Result<String> {
    if json.is_object() {
        let object = json.as_object().unwrap();
        let mut sorted: Vec<(&String, &Value)> =
            object.into_iter().collect();
        sorted.sort_by(|a, b| a.0.cmp(&b.0));

        let mut string = "{".to_string();
        for (i, (key, value)) in sorted.iter().enumerate() {
            if !i==0{
                string.push_str(", ");
            }
            let key_formatted = format!("\"{}\":", &key);
            string.push_str(&key_formatted);
            let value_str = json_to_string(value)?;
            string.push_str(&value_str);
        }
        string.push_str("}");
        Ok(string)
    } else if json.is_array() {
        let array = json.as_array().unwrap();
        let mut string = "[".to_string();
        for (i, value) in array.iter().enumerate() {
            if ! i==0{
                string.push_str(",");
            }
            let str_value = json_to_string(&value)?;
            string.push_str(&str_value);
        }
        string.push_str("]");
        Ok(string)
    } else {
        to_string(&json)
    }
}

で、肝心のsha256はこちらにて自炊しております。上のhash_jsonがjavascript側からアクセスする関数になります。

Rustで書かれた物をwasmにコンパイルする方法はこちらにすごく綺麗にまとまっています。ちなみに今回のテストではこの方法で得られた.wasmファイルをさらにbinaryenなるツールで軽量化・高速化しております。もはや黒魔術ですね。

で両者を10回回した時にかかった時間を平均すると(firefox,i7-8700,linux 18.04)

jsrust
ave.(ms)26.22.4

ざっと10倍早くなりました。やったね。でもこれで終わりません。普通のベンチマークテストでは10回と言わずもっとたくさん回します。試しに、1000回回して、それを10回やった平均をとってみましょう。

jsrust
ave.(ms)849.1954.5

なんとjs版のが早い、なぜ!

これがインタープリタへの膨大な投資の効果だと、っていうかそれはChromeでその投資に対してNPO的に働いている小さいMozillaが必死にその淘汰圧を受けてもなお生きながらえていることを承知で、中途半端なデザイナー上がりのへっぽこプログラマーが何晒し上げてんだ、いてまうぞ、コラと言われている感じですが、(実はfirefoxもrustが使われているし、てかrust作った団体だし、当のMozillaがWebAssemblyを激推してるんですが)

原因はJITの仕組みです、(あまりの投資によって)Javascriptインタープリターは同じような計算が出てくると、それをキャッシュして次回も実行しやすいように、どんどん回しながら早くなっていく能力を身に付けたのです。ウォームアップとか言われますが、 Javascriptは早くなっていくのです。対して、コンパイラ型はどっちかというと定常で、いつ回してもスピードに変化が出にくいようです。

ふんじゃあ、rustツカエネwasmツカエネなのかというと謎で、1000回回す関数がそうそうあるかというと疑問です。アニメーションみたいな無限ループっぽいものがあれば別ですが。(CityScienceではそうゆうことも多々あります)rust->wasmでwebglするチュートリアルもあったので、それをみてみるのは楽しいかもしれません。

一方で基本的にわざわざ変態言語で開発する必要はないかと思います。例えばバックエンドでrustで書かれた資産がすでにあって、それを少ない労力でエッジがわのブラウザで回したいと思った時は使えるでしょう。

ディープラーニングなんかC++で書かれたTensorFlowをjs側に移設する試みはすでにありますし、それもトレーニングデータを元に分類器をただ使うだけみたいな使い方にはぴったりでしょう。

あとは、bitcoinの宝くじマイニングゲームをブラウザ側にやらせるのは数年前は流行りましたが、基本的にGPU側の問題なので、そーんなに使われなさそう。そもそもPoWどうなんだってこともありますしね。

wasmもいきのこりをかけて、さらに軽量化・高速化の提言が出てますし、rustもどんどん浸透していくようで(windowsの開発にも使われたり)状況も刻々と変わりますがとりあえずこんな感じでした。