はじめに
この記事は、WASM(WebAssembly)を触ったことがない私が、大学で所属している研究室のテーマがWeb系で、「先生が何かとWASMの話を出してくるから、ちょっと触ってみようかな〜」という軽い気持ちで触ってみたWASMの話をクリプトジャッキングのお話を交えながら()書いてみようかな〜という記事です。
また、最後にはついでにWASMの導入方法とWASMのメリットを活かすべくJavaScript VS C++(WASM)を比較してみた結果とか書いていきます。
最後に実行した話も書くという事で、一応私の環境は以下の通りです。
- チップ: MacBook M2 Pro 2023(14インチ)
- メモリ: 16GB
- macOS: Sequoia 15.5
1. WASM(WebAssembly)とは?
1.1. 前提としてWeb技術のおはなし
まず前提としてWebブラウザ動く言語はJavaScriptしかありません!!!(語弊)
はい。この時点で 「おまえは何を言っているんだ」 と言いたいベテランエンジニアの先輩方、大変申し訳ございません。私の語彙力では「実は地球って自転しててェ(地動説)」みたいな簡潔な説明しかできませんでした。ですがはまぁWebについて最初はこんな感じの解釈でいいんじゃ無いかなと思います。
Webといえば、
- JavaScript / TypeScript
- PHP
- Go
- Java(Servletとかその辺のやつ)
- えとせとら♡
みたいな言語いっぱいありますよね?
でもブラウザ上で動くのは先ほど言った通りJavaScriptしかなくて、その他の言語ってサーバー側という港で動作して、計算やら処理やらした結果を組み込んだHTMLやら組み立てて、やっとインターネッツなブラウザという大海に乗り出すんです。わからんかったら「Webを支える技術(山本 陽平 (著))」でも読んでみてください。これマジでおすすめです。
上記で挙げた言語ってまぁ使えるし初心者目線からしたら別に普段使いする分には支障ないですよね。
ですけどもぉ皆さんってぇ、プログラミング触り始めた時に「プログラミング言語人気ランキング」とか 「プログラミング言語実行速度ランキング」 みたいなのめっちゃ気にしませんでした???私にもそういう時代がありました。
やっぱ実行速度の王様、「C++」 をWebブラウザで使いたいなぁ〜とか思っちゃったりするわけです。普通に計算速度要る技術いっぱいあるからね。
1.2. WASM「オンギャァオンギャァ(爆誕)」
そして時は2015年、WebAssembly という技術が誕生しました。
この技術を使うと、昔懐かしのFlashみたいな感じですよもう。仮想的な実行環境下でバイナリ化されたC++くんが暴れ牛の如くイキイキ動いてくれるんですね。
それ以外にもJavaだったりRustだったりetc...がインターネッツなWebブラウザの大海に乗り出しても動作してくれるようになりました。
しかもただ動くだけじゃなくて、ブラウザ上で 「速さ」 と 「安全性(?)」 を兼ね備えた実行環境が実現されたっていうのが超すごいポイントなんですな。
2. くりぷとじゃっきんぐ
2.1. ブロックチェーンってのがあってだな...
皆さんはブロックチェーンって知ってますかい?知らない?そう...(無慈悲)
ブロックチェーンっていうのはビットコインとかに使われてる技術でしてね?一箇所に集約して管理する中央集権型じゃなくて1人1人がお互いを知ることでみんなで作り上げましょうやってな感じの技術なんです。
だから大きいサーバー1箇所がタヒんでも他が生きてる限りは動き続けるんです。私はこれ個人的にフ○メタル・パニック!のアマルガムみたいだなぁと思いました(小並感)
んでほぼ改ざんできないし、分散してるから耐障害性がすごい高いんです。
では色々あるけど今回重要になる部分のこの技術の概要を説明しますね。
例えば、
- ハッシュ値
- データ
- ナンス(nonce)っていうなんか数字
っていう情報を一つのブロックにまとめるじゃないですか?それをね、いっぱいいっぱい繋げて連鎖させるんですね。チェーンみたいですね。
...ん?ブロック がぁ...チェーン みたいぃ...?
「ブロックチェーン」...ってコト!?
上記の説明画像は ブロックチェーンってなんぞや? を200行のコードで実現している「Naivechain」っていうのを参考にした図です。
2.2. 計算量どないなっとんじゃい!
ブロックチェーンって、改竄されないためにハッシュ値の計算を要するんです。
そのハッシュ値にも条件があってですね。今回は「ハッシュ値の最初の部分が"00000"から始まること」を条件としましょう。
そしてナンス(nonce)っていう値を初期値0としましょう。
手順として、
- 前のブロックの情報とかと自分のブロックの情報をまとめてハッシュ化します。
- ハッシュ値が「000abcdeffffff」みたいな内容だったとします。
- 「00000」じゃないのでやり直しです。ここでナンス(nonce)っていう値を使います。ナンスを
1にしましょう。 - 前のブロックの情報とかと自分のブロックの情報にナンスを加えたものをまとめてハッシュ化します。
- ハッシュ値が「0000bcdefabcde」みたいな内容だったとします。条件満たしてないですね。
- ほなナンスを1増やします。
- ハッシュ化に戻ります。
みたいなの一生繰り返して条件満たすまで計算するっていうのがブロックチェーンの計算です。
条件を満たしたハッシュ値が見つかるコトで初めてそのブロックはブロックチェーンに追加してもいいよ〜って認められます。それまでは蚊帳の外ってわけです。
これはめっちゃざっくりとした説明なので簡潔にしてますが実際はもっとエグデカい数字やら複雑な計算が絡んでます。
2.3. 計算量多いなぁ...せや、他人に計算させたろ!
私は昔、ゼミの課題で「Naivechainってのを動作させてみてどんなもんなんかレポートまとめてね〜」というのがありブロックチェーンみたいなものをM2 ProチップのMacBookで動作させてみたことがあります。
ブロックが3,4個の時は「ちょっと重いなぁ」ぐらいで済みましたが10個ぐらい繋げると私の愛しのMacBookくんがホッカイロの如くぬくもりを帯びてしまうという驚異の計算量をしていました。
こんな風に簡単なブロックチェーンですら膨大な計算が必要になる中、ビットコインで一山当てたい方々はこのハッシュ値を得るべくマシンフル稼働で計算しまくっていました。これをマイニングって言うんですね。
「計算量膨大やしワイのマシンだけやったらリソース足らんし効率悪いなぁ」と考えたワルなお方、思いついてしまいました。他人のマシンで計算させればいいじゃない! と。
そこで登場するのがWASMです。
WASMではめちゃ速な実行速度やメモリ効率の良い言語が使えましたよね?てか基本的に速度速なりましたよね??
他人のマシンに向かって攻撃して権限を奪って計算させる…というハッキング行為をしなくても、ブラウザでアクセスされた瞬間に、WASMを使って勝手に計算を始めることができてしまうんです。
これがいわゆるクリプトジャッキングの仕組みの一つです。
つまり、悪意あるウェブサイトが利用者の知らない間に、WASMでCPUをフル活用して仮想通貨のマイニングをさせることができてしまうんですね。
この仕組みがわかると、エチチなサイトとかあまりよろしくないサイトを踏んでしまった時に「CPU稼働率半端ないってェ!」とか「急にパソコンが熱くなった!」っていう現象の背景が若干見えてきます。
3. WASM使ってみよう!
導入・使用方法はMDNのドキュメントの導入方法を参照してください。まぁ簡単なので「入れ方ぐらい分かるわ!!💢」って方は4章まで飛ばしてください。
3.1. WASMのC/C++用コンパイラ入れてみよう
導入方法はEmscripten公式のドキュメントの通りです。
C言語やC++で書いたものをWASMで使うにはEmscriptenっていうのを使ってコンパイルする必要があるみたいです。
まずはそのためにSDKを取得していきます。
今回入れるコンパイラを格納するディレクトリを作成しましょう。(別に要らないかもしれない)
$ mkdir c_cpp_wasmCompiler
$ cd c_cpp_wasmCompiler
手動だとZIPファイル取得してきて解凍する方法がありますが公式ドキュメントに則ってgitコマンド使っていきます。
ではここにGitHubからSDKをぶちこみます。以下のコマンドを実行してください。
$ git clone https://github.com/emscripten-core/emsdk.git
クローンに成功したらemsdkディレクトリに移動してください。
$ cd emsdk
次に
- emsdkを最新バージョン取得しとく。
- SDKツール一式インストールする。
- 最新SDKツールをアクティブにする。
の3ステップ一気にいきます。
$ git pull # 1つ目
$ ./emsdk install latest # 2つ目
$ ./emsdk activate latest # 3つ目
これでSDKの取得は完了です。
もしこのまま開発に取り掛かるなら現在開いてるターミナルでシェルをアクティブにする必要があります。
$ source ./emsdk_env.sh
上記コマンドを実行したターミナルでのみコンパイルができるようになります。
「別ターミナルでも実行したいけど毎回シェル呼ぶのめんどくさい」という方は次節を参照してください。
3.2. zshの設定
zshを使っている方は、毎回ターミナルを開くたびにSDK環境を有効にするのが面倒なので、~/.zshrcに以下の一行を追加して自動で有効化されるようにしましょう。
$ echo 'source ~/<いい感じに絶対パス>/emsdk/emsdk_env.sh' >> ~/.zshrc
例えば3.1節の通りに~/c_cpp_wasmCompilerで作業を行った場合は
$ echo 'source ~/c_cpp_wasmCompiler/emsdk/emsdk_env.sh' >> ~/.zshrc
という風に書きます。
設定を反映させるにはターミナルを再起動するか、
$ source ~/.zshrc
を実行してください。
これで別のターミナル開いても毎回設定する必要なくなるはずです。
4. JavaScript VS C++(WASM)
ここからは実際に JavaScript と WASM(C++) で同じ処理を実装して、実行速度の違いを比較してみます。
せっかくWASMを使うなら、やっぱり「どれくらい速いの?」というところが気になりますよね。
4.1. 赤コーナーJavaScript
まずは、単純に「0からiterations回までの数値を足し算する」という処理をJavaScriptで書いてみます。
//sumJS.js
function sumJS(iterations) {
let sum = 0;
for (let i = 0; i < iterations; i++) {
sum += i;
}
return sum;
}
この関数はiterations回ループし、その間に単純な加算を行います。
ブラウザのコンソールやNode.jsで計測可能です。
4.2. 青コーナーC++
4.1節同様の処理をC++で実装していきます。
//sumCPP.cpp
#include <emscripten/emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
long long sumCPP(int iterations) {
long long sum = 0;
for (int i = 0; i < iterations; i++) {
sum += i;
}
return sum;
}
}
こっちはWASM用にコンパイルする必要があります。
以下のコマンドを実行してあげましょう。
$ em++ sumCPP.cpp \
-s EXPORTED_FUNCTIONS='["_sumCPP"]' \
-s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-o sumCPP.js
4.3. HTML
やっぱGUIで確認できるようにしたいですよね。
軽いHTMLを作成していきましょう。
<!-- battle.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>JS vs WASM Speed Test</title>
</head>
<body>
<h1>JavaScript VS WASM(C++) 実行速度比較</h1>
<button id="jsBtn">JavaScriptで計算</button>
<button id="wasmBtn">WASM(C++)で計算</button>
<pre id="result"></pre>
<!-- JSの計算ロジック -->
<script src="./sumJS.js"></script>
<!-- WASM (Emscripten output) -->
<script src="./a.out.js"></script>
<script>
let sumCPP;
Module.onRuntimeInitialized = () => {
sumCPP = Module.cwrap('sumCPP', 'number', ['number']);
};
const iterations = 1_000_000;
const resultArea = document.getElementById("result");
document.getElementById('jsBtn').addEventListener('click', () => {
const start = performance.now();
const result = sumJS(iterations);
const end = performance.now();
resultArea.textContent += `JS result: ${result}, time: ${(end - start).toFixed(3)} ms\n`;
});
document.getElementById('wasmBtn').addEventListener('click', () => {
const start = performance.now();
const result = sumCPP(iterations);
const end = performance.now();
resultArea.textContent += `WASM result: ${result}, time: ${(end - start).toFixed(3)} ms\n`;
});
</script>
</body>
</html>
さぁ戦いの場は整いました。あとはサーバー立てるだけです。
私は普段は npm を使ったり、VSCode の拡張機能「Live Server」を使ったりしています。
......なんですが、今回参加している KDIX CS アドカレ、実はこれ 授業の一環で出されている加点課題 なんですね。
で、その授業で最近ちょうど習った内容が Python3 を使った簡易サーバ構築。
せっかくなので、ここでは授業で習った方法を使ってサーバーを立てていきましょう。
以下のコマンドを実行してください。
$ python3 -m http.server 8080
これで8080番ポートでhttpサーバーが立ち上がりましたので、http://localhost:8080/battle.htmlにアクセスしていきましょう。
4.4. 検証
「結果発表ぉ〜〜〜〜!!!」
JSとWASM共に1,000,000回実行を3回ずつやってみた結果は...?
んん?何コレ?
1回目はJSが3.3msでC++が2.2ms...。
うん。C++のほうが若干早いよね。
2回目3回目はJSが0.9ms...?
んん?何コレ?(2回目)
安心してください!これには理由がありまぁす!
JavaScriptが動作するエンジン、V8くん(Chrome)は頭がいいんですね。
1回目の実行では「ふむふむ…こういうコードね」とじっくり解析しながら動くのですが、
この1回目の解析を元に JIT(Just-In-Time) 最適化してくれます。
そして2回目以降の実行では 「これ○研ゼミでやったところだァ!」 と最適化済みのネイティブコードで処理を蹂躙してくれるんですね〜
よって結論として、
純粋な言語としてのJavaScript VS C++であればC++の方が若干実行速度が速いものの、
C++(WASM)と比較すると2回目以降最適化されたJavaScriptの方が圧倒的に速い
ってことになるんですね。
いやWASMが速いみたいな話どこやねんと。
C++(WASM)は2回目以降最適化されてませんからね。コンパイルはしましたがこれはバイナリ化して完成されたものなので実行しても最適化されません。
なので今回は実行環境やら比較条件やらが色々とガバガバなことも相まってこのような結果になってしまいました...つまり、あまりにもJSにとって有利な環境だったということです。
逆にWASMの本領はもっと重たい処理になります。
画像処理とか音声処理、それこそクリプトジャッキングの話をしましたが暗号化アルゴリズムとかハッシュ計算とかでJSが普段やらなさそうな処理、最適化しきれない処理ではWASMの方が圧倒的に速くなります。
5. さいごに
ここまで読んでくださりありがとうございました!
JavaScript と WebAssembly(C++)を実際に比較してみたことで、
「C++使えるWASMが常に最強!」という単純な話じゃないというのが体感できたと思います。
- JavaScript は JIT最適化 が刺さると爆速になる
- WASMは重たい処理や専門的なアルゴリズムで圧倒的に強い
っていうのは個人的に「すげ〜。」って思えた気づきでした。
今回扱ったのは「100万回足し算するだけ」という超単純な処理で、WASMの真価は正直ここでは全然出ていません。
本当に輝くのは、4.4節で申し上げた通り画像処理・音声処理・圧縮・暗号化・ハッシュ計算とかいったようなフロントでやりたくない系の処理を投げたときです。
この記事が、
「WASM って意外と簡単に触れるんだな」
「C++ をブラウザで動かせんのおもろそうやな」
と感じるきっかけになれば嬉しいです!
また、少しでも役に立ったなら、いいねやコメントいただけると励みになります!
それでは今回はこのへんで。おつかれさまでした!

