👯

WebAssemblyの歴史について

2022/12/25に公開

はじめに

最近、Node.jsとDenoの開発者であるRyan DahlさんがJavaScript Containersという記事を書いていることを知った。

Webとの親和性の高さがサーバーサイドで求められる中、JavaScriptがユニバーサルスクリプトとして活躍するだろう。そして、コンテナランタイムがLinuxコンテナの抽象レイヤーとしてあるように、JavaScript界隈では既存のWebAPIそのものが抽象化の手助けとなるであろう、みたいな趣旨の内容だった。

彼がChromeのV8 JavaScript Engineを使ってNode.jsを誕生させた同じ頃、JavaScriptの可能性を信じて方法を模索した人がいる。Alon Zakaiさんだ。

Alon(以降、敬称略)はWebAssemblyやその考えの元になった asm.js 、 JavaScriptコンパイラ Emscripten の産みの親である。彼が asm.jsEmscripten の開発を通じてJavaScriptの可能性と限界を知ったことがWebAssemblyに繋がった。

WebAssemblyが生まれるまで、いったいどういった経緯があったのだろうか? そしてそのモチベーションは何だったのだろうか。

Alonが2020年にChromium University[1]というイベントで解説していた、WebAssemblyの歴史について補足しつつ勝手に紹介したい。

参照元の動画

The History of WebAssembly
https://www.youtube.com/watch?v=XuZt1OCCQTg

はじめに

WebAssembly(通称"wasm"=ワズム)はJavaScriptの次に加わった、Webのための2番目の汎用言語だ。

主要なブラウザでサポートされた2017年がWebAssembly誕生の年とされて、2022年は5周年にあたる。

WebAssemblyはもともとWebブラウザ上でJavaScriptでは荷が重い処理を高速に実行できるバイナリフォーマットとして開発された。しかし、最近ではブラウザという枠を超えて、WebAssemblyをベースとした環境が整備されつつある。冒頭で話したJavaScript Containersが描いた世界が来るとしたらWebAssemblyはその中で重要な存在となるに違いない。

このWebAssemblyが登場する少し前、Web界隈にはムーアの法則みたいにJavaScript環境が今後ずっと高速化していくだろうという期待と思ったよりは早くならないという失望が存在した。

そのきっかけとなったのが、2008年、JavaScriptのJITコンパイラが登場したことだった。

2008年、JavaScriptは高速化の道を走り始めた

  • 2008年、JavaScriptに3つのJust-In-Time(JIT) コンパイラが登場する
    • ChromeのV8
    • FirefoxのTraceMonkey
      • Firefoxの3.1頃に積まれて、3.5で正式化した
      • 以前のバージョンよりJavaScriptの実行速度が3-4倍速くなった[3]
    • SafariのSquirrelFish Extreme

結果として、2008年以降の数年間はJITコンパイラの開発競争により、JavaScriptの高速化が目覚ましい時代となる。しかし、JavaScriptは動的型付け言語である以上、当時から現環境ではその速度にも限界があることがわかっていた。JITコンパイラとは別に、Webブラウザ上でさらに処理を高速化するための方法が模索されていた。

WebAssemblyに影響を与えた二つのプロジェクト

JavaScriptのJITコンパイラが登場してからWebAssembly誕生まで、大きく2つのプロジェクトの影響がある。どちらもWebブラウザ環境を高速化するという目的のために進められたプロジェクトだった。

  • NativeClientプロジェクト
  • Emscriptenやasm.js

これらはどういったプロジェクトだろうか?

かつて、ChromeにはNativeClientという機能が実装されていた

ChromeのNativeClientプロジェクト(通称NaCl)は、独自のプロセスサンドボックスの下で実行環境に即したネイティブコードを実行することで、 高速化安全性 を両立しようとしたプロジェクトだった。

image.png
ベンチマーク結果[4]

上記の表は、SPEC2000というベンチマークを使って、通常のLinuxバイナリ、CPUの計算速度に負荷をかけるようにアラインメントを調整したバイナリ、NaClの三つで計算速度を比較した結果だ。一番右側のincreaseはNaClで動かしたことによってどれだけ遅くなったか、つまりオーバーヘッドを示している。

通常、サンドボックス上で動くアプリケーションは、その分オーバーヘッドが生じて速度が低下する。しかし、NaClではほとんどのコマンドで変わらない結果を得られた。これは画期的なことだった。

NaClの仕組みはこうだ。

image.png
NaClの仕組み

NaClは、ウェブブラウザの画面描写やWebAPIを実行するコンポーネント(左側)から、 サンドボックス 上でネイティブコードを実行して高速演算を行うコンポーネント(右側)をプラグインAPIを使って呼び出すことで計算処理を行う。

サンドボックス上で実行されるので、NaClの実行コードが任意の場所にジャンプしたり、安全ではないコードを実行してもブラウザを含む他のコンポーネントに影響を与えられることはない。

安全性良し、サンドボックスで動かしても処理は高速と、この結果だけ見れば、NaClはブラウザにおけるWeb標準となってもおかしくなかったが... 実際はそうはならなかった。

NaClのプラグインAPIというレガシー

NaClがWeb標準にはならなかった理由の1つに、NaClの根幹を成しているプラグインAPIがある。NaClが登場した時期はちょうどウェブがプラグインから脱却しつつある時期にあった。

NaClは実装当初、NPAPI(Netscape Plugin API)に依存していた。NPAPIはブラウザ拡張の標準的な仕組みを初めて導入したAPIで、動画や音声のサポートといった機能を実現させてきた(Adobe Flash Player等が有名な例)

しかし、長年の運用の結果、"ハングやクラッシュ、セキュリティ問題、コードの複雑化を招く筆頭原因になっていた"[5]。NPAPIは最終的に2014年以降段階を踏んで廃止されることになる。2011年に、MozillaとOperaがNaClとNPAPIの後継としてChromeが独自実装したPepperAPIに公式に反対したが[6]、本音はプラグインAPIそのものを捨てたいということにあった。

また、NaClには冗長な機能実装が必要だった。

NaClの設計上、NaClのサンドボックスからウェブAPIは呼び出せず、新規のウェブAPIを作成したい場合、それと同等の機能であるプラグインAPIを重複して実装する必要がある。HTMLの機能を拡張することでWeb APIの標準化を目指す動きの中で、この欠点は受け入れられなかった。 [7]

NaClの移植性という問題

Web標準に至るには、OS、クライアント、ブラウザを問わずどの環境でも動作する必要がある。当時はスマートフォンやタブレットが浸透しつつあり、PC以外でも動くことが求められるようになった最初の時代だった。この点に関してもNaClには問題があった。

NaClのコードはネイティブコードであり、WindowsPCのx86環境にはx86用の、AndroidのARM環境にはARM用のバイナリを用意する必要があり、どこでも動くわけではなかった。Googleはこの問題に対して2010年、NaClを改善したPNaCl( Portable Native Client)を発表した。

PNaClではコンパイルを二段階で行う。コンパイルの中間言語であるLLVM IRに置き換えたものを開発者側で配布し、ブラウザなどの各環境でネイティブコードに変換することで、移植性の問題はなくなった。

この変更により、PNaClでは各クライアント環境でコンパイルが必要なので起動に時間がかかるというまた別の問題が生じたが、そちらはSubZeroプロジェクトを立ち上げて解決することになる。

NativeClientは業界内で大きな注目を集めた

このように、問題をもろもろ抱えつつも、圧倒的に早いNativeClientはゲーム産業などで大きな注目を集めることになる。

  • 2010年
    • UnityがNaClに移植
  • 2011年
    • .NET Frameworkの互換OSSフレームワークであるMonoがNaClに移植
    • Bastionなど多数のゲームがNaClに移植
  • 2013年
    • Chrome31において、今までβ版にしか入ってなかったPNaClが安定版で利用できるように

最終的にWeb標準化までは至らなかったPNaClはWebAssemblyにバトンを渡す形で2018年にサポートを終了したが、その成果はWebAssemblyで生きることになる。[8]

asm.jsやEmscripten: WebAssemblyに繋がるもう1つの主役

Native Clientの歴史をここまで追いかけてきたが、WebAssemblyに繋がるもう一つのプロジェクトであるasm.jsやEmscriptenの歴史をこれから説明する。

Emscriptenやasm.jsを開発したきっかけ

2010年、Alonは、MozillaでFirefoxの開発に携わる傍ら、前職で開発してたC++のゲームエンジンをブラウザで動かせないかプライベートで模索していた。

当時、NaClは世間的に新しく、PNaClは発表されたばかりだった。NaClには前述で説明したように移植性という問題があり、C++を動かす別の方法を考える必要があった。

C++をWebに持っていくために

そこで、Alonはひらめく。 C++をそのままJavaScriptに変換すれば、ブラウザ上でも高速に動作するのではないか?

確証はなかった。JavaScriptは明示的な型宣言を持っておらず、動的な型宣言をしているので最適化が難しい。本当に高速に処理できるのか?

C++からJavaScriptに置き換えた場合、暗黙的に型が保存されていることにAlonは着目した。たとえば、C++からJavaScriptにコードを単純に置き換えた次のソースコードがある。

// C++
int x = 10;
x++;
// JavaScript
var x = 10;
x++;

C++で x に10を代入してインクリメントしている処理はJavaScriptでも同様に整数として扱っているはず。JavaScriptエンジンは高速に処理できているのではないか?

上記懸念を解決するために、実際にJavaScriptと同じように型を指定しないRPython(インタープリターを開発するために作られたPythonのサブセット)とPyPyコンパイラを使って検証してみた。

C++ → RPython → マシンコード
C++ → RPython → マシンコード

その結果、C++コードを直接コンパイルして動かしたときの実行速度とほぼ同じ処理速度であることがわかった。

Emscriptenの誕生

上記の検証を踏まえて、AlonはC++をJavaScriptに変換するEmscripten(エムスクリプテン)を開発した。EmscriptenはC++で書かれたコードをLLVM IRを通してJavaScriptに変換するコンパイラである。

C++ → LLVM IR → JavaScript
C++ → LLVM IR → JavaScript

Emscriptenは期待以上に盛り上がった

Emscriptenによって、PythonやDoomがJavaScriptとして動くようになるなど、多くのソフトウェアがEmscriptenによってリコンパイルされた。

Alon自身、次の変化があった

  • JSConfEUでEmscriptenについて講演し、Emscriptenの中で使われているアルゴリズム"Relooper"について論文を執筆した。[9] Relooperは、LLVM IRからJavaScriptに逆コンパイルする際に、Webブラウザで動作することを想定して、ループやif文のような通常の高レベルJavaScriptコードフロー構造を再現するためのアルゴリズムだ。

  • 2011年、Mozilla(の偉い人: BrendanとAndreas)から社内オファーがあり、ResearchチームでEmscriptenの開発にフルタイムでかかわるようになった

    • 補足: Brendan EichはJavaScriptの産みの親。Andreas GalはFirefoxの初めてのJITコンパイラ、TraceMonkeyのリードディベロッパーである

なぜEmscriptenが受け入れられたのだろうか?Alonは語る。「当時の皆はNaClと比較してJavaScriptへのコンパイルは"web-friendly"であり、JavaScript自体が高速化すればいつの日か、目的を十分に満たすくらいWebブラウザ環境の高速化が達成できると思っていた。」

JITの限界とasm.jsの誕生

しかし、実際のところ、普通のJavaScriptのスピードアップは十分ではなかった😢JavaScriptのJITコンパイラで行われている最適化処理は予測不可能であり、安定した性能を出すことは難しかった。

この欠点を克服したのが2013年、AlonとLuke Wagner、Davide Hermanの3人で立ち上げた asm.js である。

asm.jsはJavaScriptのサブセットであり、純粋なJavaScriptとしても動く。こんな感じだ。

function add(x, y){
    x = x | 0;         // `| 0` (or 0)を付けることで値を変えずに
    y = y | 0;         // 符号付き32bit整数型であることを型証明&強制している
    return x + y | 0;  // そして戻り値にも。
}

use asm ヒントを冒頭に付けることで、 asm.jsをサポートしているJavaScriptエンジンではasm.jsの型システムを使ってAOTコンパイルが実行される。[10] 型を動的に付ける必要があるJITコンパイルよりも、最適化処理が行われるため、高速であり性能が安定する。

Alonが自身のブログで当時公開していたベンチマーク結果を下記に示す[11]

macro4b.png
通常のFirefox、Firefox+asm.js、ネイティブコード(そして、当時asm.jsを未サポートだったChrome)で処理速度を比較した図

clangでビルドしたネイティブコードと比較しても、最終的にJavaScript環境でも性能面で50%まで迫ることができた。当然Native Clientのスピードには到底及ばなかったが...

asm.jsの限界: なぜネイティブコードと同じ性能が出なかったのか?

なぜasm.jsではネイティブコードと同じ性能が出なかったのか?

asm.jshack だったから」とAlonは語る。「JavaScriptを使ってAOTコンパイルをする方法を見つけたことは興味深かったし、その過程は楽しかったけど、hackには変わりない。あの |0 の書き方は奇妙で見た目も変だし、JavaScriptが意図した使い方ではなかったのは確かだ。ブログでそのことを真っ当に指摘した人も中にはいたけど、彼らは正しかったと思う。」

asm.jsを動かすためにdoubleをfloatに変換する Math.fround などJavaScriptに仕様追加したことで、純粋にJavaScriptにとっても意義がある改修もあったが、一方でコンパイラは必要とするが、JavaScript自身には追加しても意味がない仕様もあり、asm.jsに合わせてすべての仕様追加を行うわけにはいかなった。

結果として機能が複雑になったり、JavaScript故に起動時間が遅かったりとhackであるがゆえに欠点がいくつもあったが、asm.jsは全体的に成功を収めることになる。

asm.jsもまた業界内で多くの関心を集めた

asm.jsは欠点があるにもかかわらず、ちゃんと動作したこと、すべてのブラウザーで動いたことから、業界内の多くの関心を集めた。

  • 2013年
    • Unreal Engine3が移植される
  • 2014年
    • UnityがWebにおけるUnityの未来だとしてEmscripten/asm.jsに注力することを告知
  • 2015年
    • Firefox以外のブラウザ、EdgeとChromeでもasm.jsの最適化を行うことを発表

Native ClientとEmscripten、asm.jsのコラボレーション、そしてWebAssemblyへ

これまで、Native ClientとEmscripten、asm.js、それぞれの背景について説明してきたが、両者間の関係はどうだったのだろうか?

Native ClientとEmscripten、asm.jsは、Webを高速化したいという目的で競合するライバルではあったが、一方で開発者同士は協力関係にもあった。

2013年には、当時GoogleにいたJF Bastienが始めたことをきっかけとしてGoogleとMozillaのツールを開発しているメンバーが、ランチやディナーで月1でミートアップを行い、いろいろと話をするようになり、次第に両者のプロジェクトでコードを共有するまでになった。

例えば、NaClの人たちが作ったPepper.jsはEmscriptenを使ってNaClのプロジェクトをasm.jsにコンパイルできるようにしたツールである。これによってNaClのコードの移植性の問題を解決し、コードベースを一つにまとめることができるようになった。(NaClは他のブラウザで対応してなかったので複数のコードリポジトリが必要だった)

また、2014年、FirefoxのLuke WagnerやChromeのBen Titzerなど、ブラウザチームでasm.jsの最適化に取り組んでいる人たちが、課題について話しているうちに議論が進展し、Webにとって最適なソリューションはバイトコード(bytecode)を活用した設計であると共通認識を得るようになった。(バイトコードは、VMのために設計されたプログラミング命令コード体系を指す。Javaのクラスファイルが有名な例だ)

その結果、2015年3月に行われたGame Developers Conference、GDCの期間中に、GoogleやMozillaなどの開発者が集まり、当時はWebAsmと呼ばれていたものについて議論し、ウェブの新しい言語を標準化するために、計画はますます具体的になっていった。

後年、2015年4月1日のGoogleとMozillaの間のメールから、WebAssemblyという名前に言及したと思われる最初のメールが見つかり、この時期には明確になっていたと思われる。

同年6月、Mozilla、Chromium、EdgeおよびWebKitのエンジニアが連名でWebのための新しい言語、WebAssemblyを標準化することを発表した。[12]

二年後の2017年、WebAssembly Version1.0は、4つのメジャーブラウザー(Firefox, Chrome, Safari, Edge)に搭載された。[13]

2015年、何が変えたのか?

Alonらは2015年にWebAssemblyに取り組むことを決め、2017年に主要ブラウザのリリースをもってウェブに追加された。しかし今までも同様の取り組みや課題はあったわけで、何が変えたのだろうか?

WebAssemblyが出る何年も前から、一部のひとたちはこのように主張してきた。

「どうしてブラウザにVMやバイトコードを組み込まないのか?好きなプログラミング言語を使えるようにすればいいではないか?」

実際に、Alonが書いたEmscriptenを使ってブラウザ上でLuaを動かしてみたよという記事に寄せられたHacker Newsの当時のコメントだ。[14]

If this is the real deal, then why not ship common language VMs (in asm.js) with the browser? Then you don't need to worry about 200k at all and all code can run in the browser? I mean, if google is shipping NaCl and Dart VMs, why not? Updates could ship with browser updates, or you can still load your own rolled version if you like.

もしこれが本物なら、共通言語のVM(asm.jsで)をブラウザと一緒に出荷してはどうだろう?そうすれば、200kの心配をする必要は全くなく(訳注: asm.js+gzipでパッケージしてもLua VMは200kのサイズになったことを指して)、すべてのコードがブラウザで実行できるのでは?つまり、もしgoogleがNaClとDartのVMを出荷しているなら、なぜしないのでしょうか?ブラウザの更新と一緒にアップデートすることもできますし、好きなように自分のロールしたバージョンをロードすることもできます。

ベンダー間での標準化、コンセンサスを得るために困難な道ではあるが、これは妥当な質問だったとAlonは当時を振り返る。

「JavaScriptは色んな事に使えるが、全てではない。他に適したものが必要ならどうしてそれを使わないのか?」

その結果がWebAssemblyなわけだ。繰り返すがなぜ最終的にブラウザにVM(WASM runtime)やバイトコード(WebAssembly)を積むことになったのか?そしてそれは2015年というタイミングだったのだろうか?

一つは、NaClとasm.jsは業界における多くの関心を引いたことだ。

当時、プラグインはなくなりつつあり、代わりにWebからコードを配布&実行できる手段を皆が探していた。例えば、最初のNative Clinetに使われていたNetscape Plugin APIは2013年~2014年にかけてChromeから取り除かれてしまった[15]

そしてasm.jsが使われてしまったがために、最適化の限界とhack的な扱いによるデメリットが、(皮肉にも) もっと筋が通ったソリューションがあるだろうと自然とコンセンサスを得られる雰囲気を作ったのだ。

また、競合するブラウザと競合するツールチェーンの間で、コラボレーションが進んでいたことも大きな要因だ。同じ目的のために別々のものを開発するのではなく、コード、アイディアを共有して、新しいものを標準化するために協力しあう原動力となった。

WebAssemblyはNaClとasm.jsの良さを受け継いだ

こうして誕生したWebAssemblyはNaClとasm.jsの良いとこどりと言える。

  • Naclの良さ
    • JavaScriptをベースとせず、バイナリフォーマットを用いた、最適な新規設計を行ったこと[16]
  • asm.js の良さ
    • DOMとして同じプロセスで動作すること
    • JavaScriptを通じてWeb APIを直接的に呼び出せること
      • =>Web環境にフィットすること

その後、NaClとasm.jsは非推奨になった。2015年以降、この分野においてブラウザベンダーは合意の下、WebAssemblyという唯一の標準に集中している。

ツール側の動き

ツールの分野においても取り組みは収束しつつある。

たとえば、Emscriptenでは、pthread、OpenGL、SDL2など、さまざまなWeb APIのサポートを(独自に)何年もかけて追加してきたが、最終的にMozillaとGoogleのエンジニアは協力してWebAssemblyサポートに取り組み、LLVM BackendにWebAssemblyを追加した。この仕組みはEmscriptenの中でも、スタンドアローンでも使われている。

hoge.excalidraw.png
LLVMを挟んで各プログラミング言語がWebAssemblyに変換される様子

WebAssemblyは始まったばかり

最後に、最近のWebAssemblyの様子について説明するとエキサイティングな出来事が数多く起きている。

所感

2022年はWebAssemblyが誕生して5周年にあたる。2022年限定で存在した特設サイトでは、上記で述べた内容について時系列で紹介している。その中でもやはり目を引くのはここだろう。


WebAssembly announced

主要なブラウザベンダーがWebAssemblyについて連名で発表したそのとき、合意を得るための高いハードルを乗り越えて競合同士がWeb標準化というゴールに辿り着けたのはすごいことだと思う。

コラボレーションは素晴らしいものだ。

脚注
  1. Chromeチームの中の人が、最近のWebの動向や関連する技術の仕組みについてレクチャーするイベント ↩︎

  2. https://blog.chromium.org/2010/12/new-crankshaft-for-v8.html ↩︎

  3. https://hacks.mozilla.org/2009/07/tracemonkey-overview/ ↩︎

  4. 引用元: Native Client: A Sandbox for Portable, Untrusted x86 Native Code ↩︎

  5. https://www.itmedia.co.jp/news/articles/1411/25/news064.html ↩︎

  6. Mozilla: Our browser will not run native code↩︎

  7. HTML5のドラフトが初めてできたのが2008年。そこから6年をかけて2014年勧告が出るので、NaClへの批判が大きかった2010年~2011年はちょうどその過渡期にあたる。 ↩︎

  8. https://developers-jp.googleblog.com/2017/06/goodbye-pnacl-hello-webassembly.html ↩︎

  9. https://github.com/emscripten-core/emscripten/blob/main/docs/paper.pdf 及び JSConfEU 2011 Alon Zakai: Emscripten を参考。講演中の雰囲気を見ると、こんなものもブラウザで動くのかと興奮を持って迎えられた様子がよくわかる。 ↩︎

  10. Ahead-Of-Time(AOT) compilerは、日本語で事前コンパイラという名前の通り、実行前にマシン語に全て変換してしまうタイプのコンパイラのこと。 ↩︎

  11. 当時のブログはデッドリンクになっていた。また、講演で使っていたスライドが見つからなかったのでAlonのリポジトリから発掘してきたものを使用。 ↩︎

  12. https://blog.mozilla.org/luke/2015/06/17/webassembly/等。 ↩︎

  13. WebAssembly MVPとも言う。MVPはMinimum Viable Productの略で、顧客に価値を提供できる実用最小限の機能を持たせたプロダクトのこと。 ↩︎

  14. リンクなど具体的な説明はなかったので、頑張って探した ↩︎

  15. https://developers-jp.googleblog.com/2013/11/npapi.html ↩︎

  16. PNaClの大きな特徴であるLLVM bitcode(LLVM IR)で動作する仕組みそのものをWebAssemblyは完全に受け継いだわけではない。その理由について、GoogleのエンジニアであるDerek Schuffが直接issueで回答していた。"It turns out that LLVM IR/bitcode by itself is neither portable nor stable enough to be used for this purpose...They were not designed for the goals we have, in particular a small compressed distribution format and fast decoding. We think we can do much better for wasm, with the experience we've gained from PNaCl." (LLVM IR/bitcodeは、コンパイラの最適化のために設計されているため...(中略)...私たちの目標、特に小さく圧縮された配布フォーマットと高速なデコードのために設計されたものではありません。PNaClで培った経験を生かし、wasmのためにもっと良いものができると考えています。)
    同様のQAが WebAssemblyのウェブサイト にも記載されている(thx @igrep) ↩︎

  17. なぜMicrosoftがBlazorの開発に注力しているかは WebAssemblyとBlazor: 何十年の問題を解決する に詳しい。.NETをどこの世界でも動かせるようにするという野望の一環であり、.NETをWebの世界に持ってくるための手段がBlazor WebAssemblyなのだ。Blazorは.NETからJavaScriptを呼び出し、その逆も可能にするためのフレームワークである。かつて、その役割を果たしていたのはSilverlightプラグインだった。それは歴史の中で消えたが、以前はNetflixをブラウザで閲覧するために利用されていた ぐらい普及していた。その夢は本ページでも語ったNPAPI(とそれを使っているSilverlight)とともに消えて、WebAssemblyの力を借りて復活した次第である。 ↩︎

Discussion