Skip to content

Instantly share code, notes, and snippets.

@aoitan
Last active May 10, 2017 07:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aoitan/9120c902a9ad6355d92df58345accd87 to your computer and use it in GitHub Desktop.
Save aoitan/9120c902a9ad6355d92df58345accd87 to your computer and use it in GitHub Desktop.
series on WebAssembly and what makes it fastてきとう訳

この記事は https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/ の適当訳です。

絵で見る WebAssembly ことはじめ

WebAssembly は速い。多分あなたはそう聞いたことがあるでしょう。ですがなぜ WebAssembly は速くなるのでしょうか?

このシリーズでは、WebAssembly がなぜ速いのかを解説したいと思います。

まってまって、そもそも WebAssembly って何?

WebAssembly は JavaScript 以外のプログラミング言語でコードを書いてブラウザの中でそのコードを実行する方法です。WebAssembly は JavaScript と比べて速いと言われています。

現状、WebAssembly を使うか JavaScript を使うかといった二者択一の状況を暗示したくありません。もっと言えば、私たちは WebAssembly と JavaScript が同じアプリケーションの中でどちらも使われることを期待しています。

しかし二つを比較することはWebAssembly がどれほどの潜在能力とインパクトを持つか理解するのに役立つでしょう。

ちいさなパフォーマンスの歴史

JavaScript は1995年に作られました。JavaScript は速いことを目指して設計されたわけではありませんでした。そして最初の十年間速くはありませんでした。

その後各ブラウザはさらなる競争を始めました。

2008年になると、パフォーマンス戦争と呼ばれる期間が始まりました。複数のブラウザがジャストインタイムコンパイラ (JITとも呼ばれる) を追加しました。JavaScript が実行されると、JIT はパターンを見てより速いコードを作って実行することができました。

これら JIT の導入が JavaScript のパフォーマンスが大きく進化するポイントにつながりました。JS の実行は10倍速くなったのです。

パフォーマンスが改善されたことで、JavaScript は Node.js を使ったサーバサイドプログラミングのような、それまで考えられなかったものに使われ始めました。パフォーマンスが向上したことで、JavaScript を新たな種類の問題に使える状態になりました。

私たちは今、WebAssembly によるもう一つの進化点にいるのかもしれません。

ですから、何がWebAssembly を速くしているのかを理解するために詳細を掘り下げていきましょう。

背景:

WebAssembly, the present:

この記事は https://hacks.mozilla.org/2017/02/a-crash-course-in-assembly/ の適当訳です。

はやわかりアセンブリ

これは WebAssembly はなぜ速いのかシリーズ三番目の記事です。もしまだ他の記事を読んでいないなら最初から読むことをおすすめします。

WebAssembly がどのように動くかを理解するには、アセンブリがなにものでコンパイラがどのようにアセンブリを作るのかを知ることが理解の助けになります。

JIT の記事では、マシンとのコミュニケーションがエイリアンとの会話のようだと書きました。

A person holding a sign with source code on it, and an alien responding in binary

エイリアンの頭の中身――マシンの頭がコミュニケーションをどのように解析と理解をしているのか――を見てみたいと思います。

There’s a part of this brain that’s dedicated to the thinking—things like adding and subtracting, or logical operations. There’s also a part of the brain near that which provides short-term memory, and another part that provides longer-term memory.

These different parts have names.

  • The part that does the thinking is the Arithmetic-logic Unit (ALU).
  • The short term memory is provided by registers.
  • The longer term memory is the Random Access Memory (or RAM).

A diagram showing the CPU, including ALU and Registers, and RAM

The sentences in machine code are called instructions.

What happens when one of these instructions comes into the brain? It gets split up into different parts that mean different things.

The way that this instruction is split up is specific to the wiring of this brain.

For example, a brain that is wired like this might always take the first six bits and pipe that in to the ALU. The ALU will figure out, based on the location of ones and zeros, that it needs to add two things together.

This chunk is called the “opcode”, or operation code, because it tells the ALU what operation to perform.

6-bits being taken from a 16-bit instruction and being piped into the ALU

Then this brain would take the next two chunks of three bits each to determine which two numbers it should add. These would be addresses of the registers.

Two 3-bit chunks being decoded to determine source registers

Note the annotations above the machine code here, which make it easier for us humans to understand what’s going on. This is what assembly is. It’s called symbolic machine code. It’s a way for humans to make sense of the machine code.

You can see here there is a pretty direct relationship between the assembly and the machine code for this machine. Because of this, there are different kinds of assembly for the different kinds of machine architectures that you can have. When you have a different architecture inside of a machine, it is likely to require its own dialect of assembly.

So we don’t just have one target for our translation. It’s not just one language called machine code. It’s many different kinds of machine code. Just as we speak different languages as people, machines speak different languages.

With human to alien translation, you may be going from English, or Russian, or Mandarin to Alien Language A or Alien language B. In programming terms, this is like going from C, or C++, or Rust to x86 or to ARM.

You want to be able to translate any one of these high-level programming languages down to any one of these assembly languages (which corresponds to the different architectures). One way to do this would be to create a whole bunch of different translators that can go from each language to each assembly.

Diagram showing programming languages C, C++, and Rust on the left and assembly languages x86 and ARM on the right, with arrows between every combination

That’s going to be pretty inefficient. To solve this, most compilers put at least one layer in between. The compiler will take this high-level programming language and translate it into something that’s not quite as high level, but also isn’t working at the level of machine code. And that’s called an intermediate representation (IR).

Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language

This means the compiler can take any one of these higher-level languages and translate it to the one IR language. From there, another part of the compiler can take that IR and compile it down to something specific to the target architecture.

The compiler’s front-end translates the higher-level programming language to the IR. The compiler’s backend goes from IR to the target architecture’s assembly code.

Same diagram as above, with labels for front-end and back-end

Conclusion

That’s what assembly is and how compilers translate higher-level programming languages to assembly. In the next article, we’ll see how WebAssembly fits in to this.

この記事は https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/ の適当訳です。

はやわかりジャストインタイム (JIT) コンパイラ

これは WebAssembly はなぜ速いのかシリーズ二番目の記事です。もしまだ他の記事を読んでいないなら最初から読むことをおすすめします。

JavaScript は始め速くありませんでしたが、JIT と呼ばれるもののおかげで速くなりました。でも JIT はどんな仕事をするのでしょう?

JavaScript はブラウザのなかでどう動くのか

あなたが開発者で JavaScript をページに追加するとき目的と問題を抱えています。

目的: あなたはコンピュータに何をしたいか伝えたい

問題: あなたとコンピュータは違う言葉で話している

あなたは人の言葉で話します、コンピュータは機械の言葉で話しています。JavaScript や他の高級プログラミング言語を人の言葉のように思わないかもしれませんが、実際には人の言葉です。プログラミング言語は人がわかるようにデザインされたものであり、機械のためのものではありません。

JavaScript エンジンの仕事は人の言葉を機械がわかるなにかにすることです。

映画のメッセージみたいに、お互いに話そうと考えている人間とエイリアンがいるとします。

映画の中では、人間とエイリアンは一言一言対訳するだけではありませんでした。ふたつのグループは違った考え方で世界を捉えています。それは人間と機械にも言えることです (次の記事で詳しく説明します)。

じゃあどうやって翻訳するのでしょう?

プログラミングでは、一般的にふたつの方法で機械語に翻訳します。インタープリタかコンパイラを使えます。

インタープリタを使う場合、行ごとにその場その場で翻訳されます。

一方でコンパイラはその場で翻訳しません。前もって翻訳して記録してしまいます。

インタープリタとコンパイラどちらの翻訳方法にも良いところと悪いところがあります。

インタープリタの良いところ悪いところ

インタープリタはすぐに起動して実行できます。コンパイルの手順を踏まなくてもコードの実行を始められます。頭から順番に翻訳して実行するだけです。

なのでインタープリタは JavaScript のようなものに合っているように見えます。Web 開発者がすぐコードの実行に取り掛かれることが重要です。

そしてそれこそブラウザが最初から JavaScript インタープリタを使っていた理由です。

しかし何度も同じコードを使う場合にはインタープリタにも悪いところはあります。例えばループするときなど何度も何度も何度も同じ翻訳を繰り返します。

コンパイラの良いところ悪いところ

コンパイラにはインタープリタと反対のトレードオフがあります。

コンパイルの手順が最初にあるのでスタートアップにちょっと余分な時間がかかります。その代わりループの繰り返し毎に翻訳を繰り返す必要がなくなるのでループ内のコード実行は早くなります。 他の違いとして、コンパイラはコードを編集してもっと速く実行されるようにする機会があります。この編集は最適化と呼ばれます。

インタープリタは実行時に動くので、翻訳フェイズに最適化を計算する機会がありません。

ジャストインタイムコンパイラ: ふたつの世界のいいところどり

ループを通過するたびにインタープリタがコードを再翻訳し続ける場合のような、インタプリタの非効率なところを取り除くためにブラウザはコンパイラと混ざり始めました。

それぞれのブラウザは少しずつ違う方法でコンパイラの仕組みを取り入れました。ですが基本的なアイデアは同じです。JavaScript エンジンにモニター (またはプロファイラ) と呼ばれる新しい部分を追加しました。モニターはコードがどのように動くかを監視して、どのぐらい多くの時間動いているかと使われ方の注釈を作ります。

モニターは最初全部をインタープリタで実行します。

同じコードが数回実行されるとその部分は warm (熱心な) と名付けられます。その部分がもっと沢山実行されると hot (激しい) と名付けられます。

ベースラインコンパイラ

関数が warm になり始めると、JITはそのコードがコンパイルされるようにします。そしてそれを保存します。

関数の各行は "スタブ" にコンパイルされます。スタブは行番号と変数型によってインデックス付けされます (なぜそれらが重要なのか後で説明します)。

それはスピードアップに役立ちます。ですが私が言ったとおり、コンパイラはもっとできることがあります。最適化するために一番効率的な方法がわかるまでに時間がかかるかもしれませんが……。

ベースラインコンパイラはこれらの最適化をいくつか行います (下に例を一つ挙げます)。長い時間実行を止めたくないので、最適化にはあまり時間を掛けたくありません。

しかしながら、コードが本当に hot で、コードの実行が一気にされるなら、より最適化するために時間を掛ける価値があります。

最適化コンパイラ

コードの部分がとても hot なら、モニターはコードを最適化コンパイラに送ります。それによってより速い関数のバージョンが作られ保存されます。

コードのより速いバージョンが作られる時、最適化コンパイラはいくつかの仮定をします。

たとえば、もしある特定のコンストラクタによって作られた全てのオブジェクトが同じ形――プロパティが常に同じ名前、同じ順序で追加される――と仮定できるなら、それに基づいていくつかのコーナーケースの処理が省略できます。

最適化コンパイラはモニターがコードの実行を監視して集めた情報を使って仮定の仕方を判断します。もしこれまでのループの実行が true なら、続きも true だと仮定します。

しかしもちろん JavaScript ではその仮定に保証はありません。99個のオブジェクトは同じ形かもしれませんが、100個目はプロパティが足りないかもしれません。

ですからコンパイルされたコードは実行前に仮定が妥当かチェックする必要があります。仮定が妥当であればコンパイルされたコードが実行されます。しかし仮定が妥当でないなら JIT は誤った仮定が作られたとみなして最適化コードを破棄します。

そしてインタープリタかベースラインコンパイルされたバージョンの実行に戻ります。このプロセスを脱最適化 (または脱出) と呼びます。

通常最適化コンパイラが作るコードは高速ですが、しかし時おり場合予期せぬパフォーマンス問題を起こします。もし最適化と脱最適化を続けるコードがあった場合、そのコードはベースラインコンパイルされたバージョンより遅いかもしれません。

ほとんどのブラウザは最適化/脱最適化サイクルが起きた時にそれを打破するための制限を設けています。JIT が10回以上最適化を試して捨ててを繰り返したら試すのをやめます。

最適化の例: 型特殊化

最適化にはたくさんのちがう種類があります。しかしあなたがどのように最適化が起こるかの感じをつかめるよう一つの型に注目して取り上げたいと思います。

JavaScript が使う動的型システムは実行時に若干余分な作業を要求します。たとえば、次のコードを見てみましょう:

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

ループの中にある += のステップ (訳注:原文では += step ) は簡単に見えます。このステップは1ステップでできそうだと思うかもしれませんが、動的型付けでは思うより多くのステップが必要です。

arr は100要素の整数を持つ配列だとしましょう。コードが warm になると、ベースラインコンパイラは関数内の各操作のスタブを作ります。つまり sum += arr[i] のスタブがあり、それは整数用の += の操作を扱います。

しかしながら、sumarr[i] が整数である保証はありません。JavaScript では型は動的なので、後のループの繰り返しでは arr[i] が文字列であるかもしれません。整数の加算と文字列の連結ではとても違った操作になり、コンパイルしたマシンコードもとても違うものになります。

JIT はそのようなとき複数のベースラインスタブをコンパイルします。もしコード片が単態的 (つまり常に同じ型で呼ばれる) なら一つのスタブが得られます。多態的 (あるコードを通るごとに違う型で呼ばれる) なら、その操作を通るたびに型ごとにそれぞれのスタブをコンパイルします。

これは JIT がスタブを選ぶ前に多くの質問をしなければならないことを意味します。

なぜならコードの各行はベースラインコンパイラの中にスタブの集合を持ち、JIT はコード行が実行されるたびに型をチェックし続けなければなりません。なのでループの繰り返しを通るごとに、同じ質問をする必要があります。

もし JIT がこのチェックを繰り返さないでよいのならコードは速く動くでしょう。それは最適化コンパイラがやることの一つです。

最適化コンパイラでは、関数をまるごとコンパイルします。型チェックはループの前に移動されます。

いくつかの JIT ではさらに最適化します。例えば、Firefox では整数のみを持つ配列を特別に分類します。arr がそういった配列の一つだとすると、JIT は arr[i] が整数かをチェックする必要がありません。これは JIT が全ての型チェックをループに入る前に行えるということを意味します。

結論

JIT を簡単に言うと、コード実行を監視して hot なコードパスを最適化することで JavaScript の実行を速くするものです。これはほとんどの JavaScript アプリケーションのパフォーマンスを何倍も改善する結果になりました。

ですがこれらの改善があっても、JavaScript のパフォーマンスは予測できません。さらに JIT は実行時に下に書いたようないくつかのオーバーヘッドを増やすことになりました。

  • 最適化と脱最適化
  • モニターが記録するのと脱出が起きるときのためのリカバリー情報にメモリを使用する
  • ベースラインバージョンと最適化バージョンの関数を保存するのにメモリを使用する

これには改善の余地があります。オーバーヘッドが取り除かれ、パフォーマンスをより予測しやすくなる。それが WebAssembly がやることの一つです。

次の記事では、アセンブリについてとコンパイラがどのように動くかを説明します。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment