ひつじのにっき

mhidakaのにっきです。たまに長文、気が向いたとき更新。

asm.jsとかPNaClとかLLVMに興味あったので調べて回ったら少しだけ理解できた話

このエントリーは以下の「Webの未来 PNaClとasm.jsでカワルミライ - いま、モバイルWebの先端で起こっていること〜」というスライドへのアンサーエントリーです。

ひょんなことからまとめはじめたのですが、とりあえずタイトルにあがっているようなasm.js(あせむじぇいえす)やPNaCl(ぴなくる)、LLVMという単語が知らない人でもわかないひとがわかった気になれるように書きました。つまりわかってないやつとはエントリを書いている本人のことだよ!

PNaClとasm.jsでカワルミライ

結構ブクマが多いのでみんな気になっているんだろうなぁ、という雰囲気があります。


でも読んでみると良く分からない単語があったり、業界背景を理解していなかったりで大変でした…。というわけで自分のような「わぁ、なんか新しい波が来てる…将来に備えて必要な気がビンビンするよぉ。ふぇぇ。独力で読むのつらいよぉ…」な人がスライドを読まなくてもこのエントリを読み進められるよ!

というぐらいには思考をまとめたつもりです。まぁ自分が忘れたころに読み返す用のメモなんですけどね。

エントリの概略

今回は、スライドの論点まとめと本エントリーで言及したい技術について先に結論を書いてしまいます。あとはだらだらと書いてますのでお好きなところまでお付き合いください。

さて、このスライドではWebを軸に2つの論点をもっています。ひとつは市場的背景、もうひとつは技術的背景です。これらを組み合わせた結果、「asm.js」「PNaCl」が現実的な解として最も妥当であると結論を得ています。

市場的背景とは

ブラウザのシェアからみるパワーバランスの変化、モバイルなど新しい市場の登場をベースとしたプラットフォームの未来予測です。

技術的背景とは

Webブラウザを語る上で重要な指標となった実行速度などに見る技術的な筋の良さ、それぞれの技術がどのように生まれて、どのような未来を描いているのか、というベンチマークにあらわれない要素も加味されています。

ここまでが前置きです。前回までのあらすじです。長かったですね。このエントリでは「asm.js」と「PNaCl」、その背景にあるLLVM技術についてまとめたいのです。これらが将来のWebを支える基幹技術として立脚し続けられるか、という部分が面白いわけです。

このエントリでは主に「技術的背景」に注力して調べた内容を書いています。「市場的背景」はゲームを提供したいのか、アプリを提供したいのか、ベンダーなのか開発者なのか、立場によって切り口がかわります。当然結論も違ってくるので書きたくなったら別途改めてまとめます。たぶん開発者として何を抑えていくと幸せになれるか、という視点になるでしょうね。

Web技術について書くよ!

さて「技術的背景」に戻りましょう。注目の技術として挙げたasm.jsとPNaClは、どちらもブラウザ上で"高速に"動くことを目的にしています。ざっくりいうと

  • asm.jsとは、ただのJavaScriptですが高速化を強く意識しています。Mozillaによって推進されています。
  • PNaClとは、JavaScriptではありません。ネイティブモジュール(バイナリだよ!)です。現在のChromeではx86とARM向けが提供されています。

つまり、PNaClは対応ブラウザがなければ動かない、新しい技術である。
asm.jsは古い技術をベースにしているため、JavaScriptをサポートしているブラウザ全てで動く。
という差があります。もちろんasm.jsにもデメリットはあるのだけどややこしいので後述。

彼らの目指すもの。まずは動いてるものを見てみよう

せっかくなので未来を先に感じたら良いと思います。みんな大好き「UnrealEngine」のasm.js実装(つまりJavaScript上で動いている)デモがこちら。

以下はスクリーンショット。↑のリンクからアクセスしてください。

このデモはasm.jsとWebGL(ブラウザで3DCGを表示させるための標準仕様。何でもできる魔法の杖ではなくOpenGLのようなAPI集です)を組み合わせたものです。
ブラウザごとにスピードが違うのでFirefoxChromeで比べてみてください(Operaでもいいんだけど、彼らはChrome/Blinkベースにするよ!って言ってるのでほとんどChromeだよね)。

もちろんasm.jsを推進しているのはMozillaなのでFirefoxが最もヌルヌルと動くわけですが、それは些細なことです。
ここで大事なのはこのような世界をブラウザを作ってる連中(MozillaGoogle)が割と真面目に血眼になって目指している、という事実です。

パフォーマンスをどうやって上げるか、それが問題だ。

要求としてパフォーマンスを求めていることは前述のとおり(市場の要求の話が出てくるべきですが、面倒なので割愛。要はPCの性能が上がっていくようなものです)。

ではどのようなアプローチで要求を満たすか。asm.jsとPNaClの技術的背景の違いはアプローチの違いでもあります。

Mozillaの考え方。asm.jsの基本思想。

Mozillaはこう考えました(※これは妄想です)。
「いやぁ、ぼくはかねてからインターネットを健全でより良いものにしたいと考えてたんだよね」
「いま市場の要望はパフォーマンス向上だよね。でも健全じゃないとまずいぞ。Webは公平にみんなが使えなくちゃ」
「パフォーマンス向上はオープンな仕様で決まったオープンな合意に基づいた技術によって達成されるべきだよね」
「独自技術を導入するとブラウザの互換性問題とかちょーひどいよね。JavaScriptベースでイケる方法あるんじゃね?」

じゃあ、パフォーマンスを向上させる asm.js って何さ

asm.jsはJavaScriptのサブセットとして設計されています。サブセット(部分集合)なのでJavaScriptとして完全に動作します。
しかしながら高速化を前提とした「制約」がいくつか存在しています。「制約」を説明するためにはJavaScriptさんの言語的背景について多少なりとも知っておく必要があります。いちどasm.jsって何さ、を横においてJavaScriptを見てみましょう。

JavaScriptについて雑な解説

JavaScriptの型付けは弱く、動的(ダック・タイピング)です。
門番「お前何者だ!」
var「文字列です!」
var「あ、いま数字になりました!」
門番「よし、通れ!」
ぐらいざっくりしてます。変数の中身なんか、前後の命令次第で変わるため評価は実行時に行う必要があります。
JavaScriptってなにさ、みたいな議論もありそうなんですが、このエントリではEcmaインターナショナルで標準化されたECMAScriptの実装ということでひとつ。

インタプリタ方式で実行してた時期もあったね

JavaScriptは当初インタプリタ方式で実行されていました。つまり書かれたソースコードを読み込んだ時に逐次解釈しながら処理していきます。今からは考えられないほど低速でした。もっとも、そのときのWebはJavaScriptで表現力が増えて満足!実行速度が求められるのはまだ未来のはなしです。

JITコンパイラ使って高速化しようぜ。

そんな中、「変数の型っていってもそうバンバン中身入れ替わらないし、文字列いれたらたいてい最後まで文字列だろJK」と考えた偉い人が出てきました。実際問題、プログラミングしてる最中に型がバンバンかわるコード書いたやついたら引っぱたきますよ。そりゃ。そういうのを真面目に統計処理して「まーこんなもんだろ」で型を推測した手法をあみだす人がいたり、「たしかにまぁ型が安定してりゃいいだろうけど実行時にコードをコンパイルして速度稼げるんじゃね?」とJavaScriptの実行時にコードのコンパイルしようぜ、という人がいたり。

かなり雑な解説ですが、これがJITコンパイラ(Just-In-Timeコンパイラ、実行時コンパイラ)の始まりです。
JITコンパイラを持つJavaScriptの処理系で有名なのが

です。C++で記述されています。なのでいくら早くJavaScriptを処理しようとネイティブの動作速度は超えられません。でも結構いいところまで早くなりそうですよね。

われらがWikipediaの該当記事

ほんとにネイティブこえられないの?稀に超えたとかいってる記事みるんだけど。

ちょー最適化されたアセンブラ吐いたり、ハードウェアを使う最適化命令を持ち出してこられたらネイティブを超えられるかもしれません。まぁ極論なのでネイティブさんも同じ命令セットつかえば同じスピード出るでしょ(つまりコンパイラ性能に依存)みたいな気はしてきます。

こういうのを読むと、アルゴリズム実装とかにも依存するんじゃないの?という話もあるし、JITコンパイラそのものもすごく頑張ってる雰囲気です。

Cよりも早いぜフハーハーなプロジェクトです。具体的にはNode.jsのMySQL binding部分でCよりも高速、という言及があります。どちらかというと言語特性を利用した性能差でJavaScript実装のほうが有利なシーンです。

このあたりの議論の盛り上がりを見ていると、一概にネイティブが速いと言えるわけでもないし、JavaScript実装のほうが速いともいえない、という所までついに到達した!
もはやケースバイケースなんだからその時に良いほうの実装を使えばいいだろ、ぐらいの感覚でしょう。MySQL binding部分が重要ならNode.jsを使えばよくて、Node.jsをJavaScriptだからといって避けるネイティブ原理主義もいないでしょう。

閑話休題。話が「Google Chromeの持ってるV8 JavaScript Engineの最適化がなかなか面白そうね」という明後日の方向にぶっ飛んだので戻します。

Mozilla『asm.js、君に決めた!』

ではasm.jsはどうやって最適化アプローチをとっているのでしょう。話をasm.jsに戻すと、Mozillaがとった高速化アプローチ(の一部)がAOTコンパイラです。Ahead-Of-Timeコンパイラ、事前コンパイラのことです。つまり、普通のC言語と一緒。先にコンパイル終わらせちゃおう。これ最強。
「お、そ、いー!」
「実行時にコンパイルするとか時間かかりすぎ!」
早くなるための制約ならあえて受け入れよう!という提督の男気ある割り切りを持ってます。

ここから先は雰囲気をつかむための例ですが、ECMAScriptで標準化されている内容には

var a = 1;

って書いてもいいんだけど、これだとaはNumberになっちゃうんだよね、だから

var a = 1|0;

ってかくと内部的に型をInt32として扱うよ。みたいなルールがあるので、こういうのをうまく使います。

asm.jsでは「a|0みつけたらintで事前コンパイルしちゃおうぜ」っていう発想がベースです。なのでJavaScriptファイル見つけたら実行前にコンパイルやっちゃってバーンって実行する=速度早い!正義だ!というロジックになります。アノテーションのような指示文法も存在します。

asm.jsのメリットとかデメリットとか

これまでの議論を踏まえると、asm.jsは「JavaScriptプログラムをネイティブコード速度の2倍程度の実行時間で済むように最適化するJavaScriptサブセットである「asm.js」」と理解するのがよさそうです。

基本的にはJavaScriptエンジンで処理速度が上がるように最適化するだけであり、うまい具合に最適化しやすいコードを書かなければいけない。もちろん普通のJavaScriptも実行できるが普通なので最適化される部分は少なく、実行速度が出ない…。だんだんデメリットもわかってきました。

つまり、制約が強すぎるので手でasm.jsに最適なコードを書くのはつらぽよ感ハンパないのです。ガンジーでも助走つけて殴るレベルでしょう。いまからアセンブラでコード組むようなものです。理論上、出来んことはないがやらん。みたいな感じの話です。

あとJavaScriptのサブセットなのでJavaScriptとして動作する利点もありますが、基本的にJavaScriptが持つ制限はそのまま残ります。シングルスレッドとかですね。WebWorker的なものと組み合わせる、という感じで回避したいのかもしれません。

asm.jsを使うには"ファイル先頭か個別機能の先頭に「use asm」と書いてオプトインするだけでよく、各エンジンに事前に最適化されるようになります。"
という具合に専用のスイッチが用意されています。一番最初に紹介した「UnrealEngine」のasm.js実装もそういう感じなんでしょうね。

ここまで読み進めたのなら次のasm.jsのFAQもすんなり理解できるんじゃないでしょうか(英語ですが面白いので興味のある人はGoogle翻訳片手に…)。

LLVMの登場

LLVMは2000年にイリノイ大学で開発され、Low Level Virtual Machine、低水準仮想機械と呼ばれています。いろんな言語に対応できるように設計されたコンパイルのための基盤です。LLVMはどのようにプログラムを最適化してくれるのでしょうか。

LLVMの概要

さて、前述のasm.jsでは一つの疑問が残りました。人の手で書けないほど制約の強いasm.js向けのJavaScriptを誰が書くか、です。
その答えがLLVMという技術です。最適化できるコードはどう書けばいいか?という制約が明文化されている以上、機械がコンパイルして出力してしまえばいいのです。答えはとっても簡単でした。

コンパイラの仕事は、単純化すると次のようになります。


C言語コンパイラなら次の通りです。


もうお分かりの通り、意味のある入力を意味のある出力に入れ替えるだけですね。つまり、asm.jsであれば

が出来れば最高にうれしいわけです。ただGCCのような専用のコンパイラを作って維持していくには莫大な労力がかかります。さすがにしんどかったのでコンパイラのための基盤であるLLVMが颯爽と登場します。

LLVMの処理フロー

LLVMではコンパイラの基盤となるべく、入力と中間表現、出力の3つの単位でモジュール化されています。

入力部分をフロントエンドと呼び、各種言語から中間言語に変換するのが役割です。この役割を持ったモジュールの実装をフロントエンド実装と呼びます。Clangとかが有名ですね。C/C++中間言語に変換するモジュール、JavaScript中間言語に変換するモジュールなど、色々なフロントエンド実装が公開されています。

中間言語は特定のマシンアーキテクチャX86やARM、MacWindowsなどいわゆる機種への)依存が無いように設計されています。各種言語やアーキテクチャから独立した全く新しいフォーマット(LLVM-IR)というわけです。このフォーマットに対して最適化を行えば特定の言語に依存しなくても全てのコードで等しく最適化できます。

出力部分はバックエンドと呼ばれます。X86バイナリを出力するバックエンド実装であればよく知っているコンパイラと同じ挙動になりますが、C/C++言語を入力に、JavaScriptを出力する場合は言語間の変換が最適化を伴って行えることになります。注意点としてasm.jsのような言語ごとに特徴のある最適化が必要な場合は、バックエンド実装で実現しないといけません。

このようなLLVMの仕組み、仕様は公開されているので欲しいモジュールが無ければ作れます、といいたいところですがGCCを丸ごと作るみたいなものに比べてコストが安い、というだけなので結構ボリューミーだと思います。ほしい場合は気合でなんとかしてください。

LLVMに興味が持った人は

このあたりを読むといいです(というか読みました)

とくに人間でもわかるLLVMバックエンド入門はLLVM本の著者さんです。

asm.jsを支えるEmscripten

LLVMについて十分理解が深まったところでJavaScriptソースコードを提供するコンパイラEmscripten」の紹介です。EmscriptenC/C++ソースコードLLVM中間言語を経由してJavaScriptに変換します。この時点ではasm.jsも関係なく、単純にJavaScriptとして動作する変換器なわけですが、asm.jsと組み合わせることで最適化が働き相乗効果が生まれます。

C/C++から、というのはゲームなどのアプリケーションでよく使われる言語なのでフロントエンドとして一番最初にターゲットに選んだのは妥当性があります。言語の壁はより小さくなっていく方向なのかもしれないです。

4gamerにあるGDC2013の記事が非常にわかりやすく解説してありますので一読するとよいでしょう。

またEmscriptenはNode.jsなどサーバ側でも使われています(十分に速く動作する、という状態になりつつある)。

PNaClが描く未来

すこしJavaScriptについての背景が長くなってしまったのですが、JITコンパイラLLVMという技術要素も合わせて説明できたのでPNaClの説明はかなり楽になりました。

PNaCl(ぴなくる)はGoogleの開発したPortable Native Clientです。つまりWebブラウザ内でネイティブコードを実行しよう、というアイデアに基づいた技術です。
どうしてそのような発想になるのでしょうか?ここに非常に興味深いグラフがあります。

JavaScriptベンチマークスコアです。

http://arewefastyet.com/#machine=11 from 冒頭のスライドより(http://www.slideshare.net/KeiNakazawa/web-pnaclasmjs-web)

どう考えても頭打ちです。本当にありがとうございました(ついでに軸がテキトーすぎんだろ、このグラフ!見た目よりは割り引いて読んでください)。つまり、JavaScriptによる最適化はほぼやり尽した、ということです。僕の知らないところで技術が始まって終わっていた!と調べた後で気付いたあたり衝撃です。あとSafariさんのやる気のなさも素晴らしいですね。AppStore(ネイティブアプリ)を守るためと邪推してしまいますがモバイル/デスクトップ共に自分のマーケットがあることで思うところがあるのかもしれません。GoogleはPlayマーケットがネイティブですがChromeアプリはブラウザベースですのでそのあたりに積極的にいける事情があるんでしょうか。

Googleの考え方。PNaClの基本思想。

Googleはこう考えました(※これは妄想です)。
「いやぁ、ぼくはかねてからユーザー体験を大切にしたいと考えてたんだよね」
「いま市場の要望はパフォーマンス向上だよね。でもちょっとまて、JavaScriptもいいけど将来頭打ちになりそうだよね?」
「このままだと難しいぞ…。そうだネイティブならもっと早くできる!より革新的なサービスができればユーザーは喜ぶさ!」
「ネイティブは独自技術だって?僕らにはみんなが使ってくれるブラウザがあるじゃないか!大丈夫、すぐに広まるよ!」

PNaClはネイティブ志向

冗談はさておき、PNaClはNaClというベース技術から発展しています。NaClの時代にはLLVMを利用しておらず、主なターゲットはX86などのデスクトップです。ネイティブコードをウェブブラウザ上で安全に実行するサンドボックス技術として活用されていました。CPU依存があったため、コンパイル済みのバイナリは1つのアーキテクチャでしか動かず、汎用性がイマイチだったのです(ブラウザの向こう側にいる相手の環境に合わせて複数バイナリを持たなければいけない)。

PNaClはアーキテクチャ依存をなくし、モバイル用途を視野に発展させたものです。アーキテクチャ依存をなくす部分にLLVM技術を活用しています。PNaClの配布形式はLLVM中間言語LLVM-IR/bitcode)です。ダウンロードした端末で、初めてネイティブコードに変換します(つまりLLVMバックエンド実装を持っていることになります、このあたり、AndroidのARTとかも似てるので興味深いと思いませんか?この話は別エントリーかな)。

ネイティブで動かすことはそんなに大事なことなのでしょうか。既存の資産が使える、というメリットはLLVMの発展により薄れていますが、開発者的にうれしいところはJavaScriptと比べたときにマルチスレッドプログラミングが出来る(いまもあまり使われていないスマートホンに積まれたやたら多いCPUコアを生かすことができる)点でしょうか。速度を追い求めるには最も理にかなった(そしてGoogleらしい)やりかたです。

あとは組込エンジニア的に感じることがひとつ。「ハードウェア」との距離が一気に近くなる点です。いわゆるネイティブAPIとの距離の近さですね。JavaScriptベースで考えたときにWebGLなど一部のハードウェアアクセスは(需要のあるものから順次)標準化されていますが、独自でデバイスにアクセスしたい場合はちょっと距離があります。そういうものを含めて解消したいのかな、と想像。このあたりの動向はChrome OSあたりのアプローチを見ると面白いのかもしれません。当然サンドボックス的にまもられてるのでAPIを整備してくると本腰を入れたという理解でよさそうです。

GoogleJavaScriptを捨てるつもりは毛頭ない

Googleがユーザー体験のために速さを追い求めることは間違いありません。ググったときの検索スピードをみていればわかりますね!
前述のグラフでV8がもっともスコアが高い(そういうテスト選んでるだけなのかもしれないけど…)ことからも速さを意識しているのは間違いありません。現行の技術でも一番を維持しつつ未来への投資(PNaCl)も行うということでしょう。ここ最近はGoogleベンチマークにasm.jsを加えたことも話題に上りました。

記事中にはasm.jsへのサポートを開始するのでは(=AOTコンパイラの搭載)、など憶測がありますが実際はそういう理由ではなくChromeJITコンパイラで十分に速い、という証左と考えている気がします。とくに後者の記事はミスリーディング感ハンパなかった(日本語でのこのあたりへのまとまった言及は少ないのに残念)。
注目すべきはasm.js以外にもTypeScriptが追加されていることであり、この2つの追加は切り離して考えずに、JavaScript手書きする時代は終わりつつあると受け取るのが妥当かな。何らかの効率的な方法で生成すべき共通言語基盤=アセンブリのように動くべきだ、という考え方は現在のトレンドと一致するように感じます。

このあたり資料少なすぎ

探してみましたが、ほっとんどない。asm.jsもあんまりなかったけどどこに行けばこういうのって見つかるんだろうか。LLVMもないですよね、ホットなジャンルなのにもったいないなぁ。

未来への投資

いかがだったでしょうか。かなり頑張って調べたのでまとめられただけで満足です。asm.js、PNaClどちらも課題解決に向けた興味深いアプローチだと思います。またLLVMにより、ネイティブとの関係も一段発展している環境が素晴らしい。個人的にはPNaClに技術的な筋の良さを感じました。

組込エンジニアな身には、このあたりの技術への興味からWebを見ると結構面白い景色なのかもしれません。Webとネイティブ、モバイル機器は同じ方向を向いていると言えなくはないかもしれない近さがありますね。LLVM本の読書会とかもしてみたいですね。