WebAssemblyの紹介


この記事は、2017年10月14日にリャザンで開催されたITSubbotnikでのスピーチに基づいています。 ロシア語では、このトピックに関する資料がまだかなりあります。この記事がお役に立てば幸いです。


免責事項:著者はWebAssemblyまたはJavaScriptの専門家ではありません。 この記事は、このトピックに関する他の人々のスピーチから導き出された考えとアイデアをまとめたものであり、数か月間WebAssemblyを学習した経験もあります。


WebAssemblyとは何ですか?


WebAssemblyWASM )-ブラウザでコードを実行できる新しいバイナリ形式。
このような定義で十分であれば、より完全な定義はWikipediaで見つけることができます。


問題


まず、WebAssemblyを作成して、解決しようとした問題やタスクを理解しましょう。 問題は決して新しいものではなく、実際にはブラウザーでコードすばやく実行することです 。 しかし、それはそれほど単純ではなく、問題自体に加えて、いくつかの関連する要件がまだあることが徐々に判明しました。



状況


この問題を解決するために、勝者が1人います。これがJavaScriptです。


敗者(完全なリストからはほど遠い):



その他の解決策:



一般に、解決の試みはすべて2つのグループに分けることができます。


解決策1:ネイティブコードをブラウザーに直接


例:ActiveX、NaCl
悪い点:移植性がない、潜在的な、または本当のセキュリティ問題。


解決策2:仮想マシンのコード


例:Javaアプレット、Silverlightなど。
悪い点:プラグインやランタイムが必要⇒ゼロ設定なし
一般に、コードのクロスプラットフォーム実行を保証したい場合、仮想マシンが適切なアプローチです。


JavaScriptの何が問題になっていますか?


JavaScriptは問題ありません。 しかし、長年にわたる生産性の成長を見ると、S字曲線の2番目の「プラトー」にあることがわかります。 最初は、パフォーマンスは小さく、徐々に成長しました。V8の登場により、かなり前にスムーズな成長に転じていた急激なジャンプが見られました。

(画像は、リンクラークによるWebAssemblyへの要約漫画の紹介から)。


最新のJavaScriptエンジンの仕組みを見てみましょう。


まず、ソースコード(JSのテキスト)はパーサーを通過します。その結果、コードの内部表現、つまり抽象構文ツリーが生成されます。 その後、インタープリターが動作します。 特定の関数は、実行中にバイトコードに変換されます-実際には、インタープリターの内部関数への一連の呼び出し。 同時に、JS関数の使用に関する統計が蓄積されます。 特定の関数で呼び出しのしきい値を超えた場合、最適化が必要であると判断され、コンパイラに渡されます。 コンパイラーはマシンコードを生成します。これは入力値のタイプに強く結び付けられています。



foo(a、b)という2つの引数を持つ関数があり、パラメーターの数値で何度も呼び出すと仮定します。 ある時点で、関数はコンパイラーに渡され、より高速に実行されます。 文字列引数で呼び出すと仮定します。 その結果、エンジンは「非最適化」を実行します。コンパイラから関数をインタープリターに戻し、完成したマシンコードが破棄されます。


それはどういう意味ですか? JavaScriptエンジンの開発者は素晴らしい仕事をしており、彼らに感謝しています。 JavaScriptは決して悪くありませんが、内部的な制限があり、根本的に高速化することはありません。


asm.js


もう1つの興味深いイニシアチブは、すでにMozilla Foundationからのもので、WebAssemblyのトピックに近づいています。 2010年に登場し、2013年に公開されました。


このアイデアは、特別なEmscriptenコンパイラを使用してCおよびC ++からコードをコンパイルできるJavaScriptのサブセットを作成することです。


これはJavaScriptのサブセットであるため、このコードはどのブラウザーでも実行されます。 さらに、最近の主要なブラウザは、asm.jsをすばやく認識し、それをプロセッサのネイティブコードに効率的にコンパイルすることができました。 C / C ++から直接取得したネイティブコードと比較して、asm.jsコードから取得したコードは1.5〜2倍(50〜67%)だけ遅くなります。


単純なC / C ++関数の場合、asm.jsコードは次のようになります。

ここで、 'use asm'は、以下のコードがasm.jsであることをJSエンジンに示すディレクティブであり、 |0の形式の構造は、整数で作業が行われることを示します(ゼロ値のビット単位のORは、Numberの小数部をリセットします)


WebAssembly開発の目標



それでは、WebAssemblyとは何ですか?



どこから始めますか? ハローワールド


WebAssemblyをマスターするオンラインのWasmFiddleツールから始めることを強くお勧めします。
(私自身はEmscriptenから始めて、しばらくしてから自分の間違いに気づきました。)


WasmFiddleインターフェース:



ソースコードは左上、ビルドボタン(テキストビューが表示される)のコンパイル結果は左下にあり、起動用のコードは右上にあり、実行ボタンの実行結果は右下にあります。


C / C ++サンプルテキスト

例として、フィボナッチ数を計算するために単純なコードを使用しました(はい、それも:)。良いコードは出会った最初のgoogleバージョンであると言うのではありません。


 int fib(int n) { if (n == 0) { return 0; } else { if ((n == -1) || (n == 1)) { return 1; } else { if (n > 0) { return fib(n - 1) + fib(n - 2); } else { return fib(n + 2) - fib(n + 1); } } } } 

テキストプレゼンテーション(WAST)について少し。 既に述べたように、WebAssemblyはバイナリ形式であり、コンパイルの出力でWASMファイルを取得します。 テキスト表現は、常にWASMファイルから取得できます。これにより、アセンブリに含まれる内容、テーブルおよびコードを正確に把握できます。 また、このビューはデバッグに使用されます。


この場合、テキスト表現によれば、1ページのメモリが割り当てられ(各ページ= 64 KB)、メモリとfib関数が表示(エクスポート)され、この関数の定義、つまり実際の実装が表示されます。


テキスト表示(WAST)

このアセンブリのテキスト表現の始まりは次のようになります。


 (module (table 0 anyfunc) (memory $0 1) (data (i32.const 12) "\01\00\00\00\00\00\00\00\01\00\00\00") (export "memory" (memory $0)) (export "fib" (func $fib)) (func $fib (param $0 i32) (result i32) (local $1 i32) (block $label$0 (br_if $label$0 (i32.ge_u (tee_local $1 (i32.add (get_local $0) (i32.const 1) ) ) (i32.const 3) ) ) (return (i32.load (i32.add (i32.shl (get_local $1) (i32.const 2) ) (i32.const 12) ) ) ... 

まとめると、例を実行するための最小限のJavaScriptコードは次のようになります。


 var wasmCode = new Uint8Array( [0,97,115,109,1,0,0,0,1,134,128,128,128,0,1,96,1,127,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,144,128,128,128,0,2,6,109,101,109,111,114,121,2,0,3,102,105,98,0,0,10,203,128,128,128,0,1,197,128,128,128,0,1,1,127,2,64,32,0,65,1,106,34,1,65,3,79,13,0,32,1,65,2,116,65,12,106,40,2,0,15,11,2,64,32,0,65,1,72,13,0,32,0,65,127,106,16,0,32,0,65,126,106,16,0,106,15,11,32,0,65,2,106,16,0,32,1,16,0,107,11,11,146,128,128,128,0,1,0,65,12,11,12,1,0,0,0,0,0,0,0,1,0,0,0]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, []); console.log(wasmInstance.exports.fib(10)); 

ここでは、完成したWASMは数字の配列としてコードに記述されていますが、実際には、WASMファイルはかなり大きくなり、サーバーからロードします。


ブラウザでのWebAssemblyの実行は次のようになります。 ブラウザーは、通常どおり、JavaScriptが実行されているHTMLページをロードします。これはすでにWebAssemblyをロードしています-「モジュール」(WebAssemblyモジュール)が判明し、モジュールのインスタンスを作成します。その後、そのインスタンスに対してエクスポートする関数を呼び出すことができます。



ここで灰色の矢印に注意してください。WebAssembly内からJavaScript関数を呼び出すことができます。 シーケンス図でこれをより詳細に検討してください。



ここでは、最初にWebAssemblyからJavaScriptを呼び出し、次にWebAssemblyからJavaScriptを呼び出します。


ここでの2番目の呼び出しでは、WebAssemblyがどのようにAPI(DOM / WebGLなど)を使用するかを示しました。 直接ではなく、可能ですが、そのような呼び出しはJavaScriptを介してのみ発生します。 明らかに、ここに「ボトルネック」があります。WASMのAPIを集中的に使用すると、JavaScriptを介してこれらの呼び出しを「スロー」する時間が大幅に減ります。


WebAssemblyメモリモデルは非常に単純です。 これは、プログラムコード、グローバル変数、スタック、およびヒープが配置されるフラットなメモリの一部です。 次のメモリ割り当てで十分なスペースがない場合、メモリの上限が自動的に増加する場合、メモリを拡張可能にすることができます。



メモリブロック全体は、バイト配列として(および16ビットおよび32ビットワードの配列として、16ビットおよび32ビットの浮動小数点値の配列として)JavaScriptからアクセスできます。 さらに、JavaScriptのメモリは読み取りと書き込みの両方に使用できます。


Emscripten


Emscriptenは、C / C ++からasm.jsとWebAssemblyを取得するためのメインコンパイラです。 (WASMには、RustやTypeScriptなどの他の言語のコンパイラもあります。)ここでは、WindowsでのEmscriptenの使用を検討しますが、他のシステムに大きな違いはないと思います。



LLVM


Emscriptenについて言えば、低レベル仮想マシン(LLVM)について少し話す価値があります。



LLVMはコンパイラーのファミリーです。 LLVMの主なアイデアは、コンパイルをフロントエンドとバックエンドに分離することです。 フロントエンドコンパイラは、ソースコードから内部表現(Intermediate Representation、IR)にコンパイルします。 IRは、一部の仮想マシンのコードです。 バックエンドコンパイラは既にIRを特定のプラットフォーム用のコードに変換しています。たとえば、x86およびx86-64のバックエンドがよく使用されます。 別のプログラミング言語のコンパイラが必要な場合は、新しいフロントエンドのみが記述されます。 新しいプラットフォーム用のコンパイルが必要な場合は、新しいバックエンドが作成されます。


EmscriptenはLLVMを使用してC / C ++からコンパイルし、asm.jsおよびWebAssemblyでのアセンブリ用のバックエンドコンパイラを提供します。


Emscriptenをインストールする


Emscriptenのインストールは非常に簡単です。私の場合はWindowsで行われ、ソースから何もコンパイルする必要さえありませんでした。


  1. ここからダウンロード: http : //emscripten.org/
  2. 私の場合はC:\bin\emsdk別のフォルダに解凍します
  3. コマンドラインを開き、emsdkフォルダーに移動して、3つのコマンドを実行します。

 emsdk update emsdk install latest emsdk activate latest 

すべてがインストールおよび構成され、コンパイルできます。 emsdk listコマンドをemsdk listインストールに使用できるすべてのツールのすべてのバージョンのリストが、現在選択されているもののマークとともに取得されます。


asm.jsでのコンパイル


Emscriptenを使用してコードをコンパイルする方法を見てみましょう。asm.jsから始めましょう。


Emscriptenの例

例は上記と同じですが、Emscripten(fib.c)用にわずかに変更されています。


 # include <emscripten/emscripten.h> int EMSCRIPTEN_KEEPALIVE fib(int n) { if (n == 0) { return 0; } else { if ((n == -1) || (n == 1)) { return 1; } else { if (n > 0) { return fib(n - 1) + fib(n - 2); } else { return fib(n + 2) - fib(n + 1); } } } } int main() { printf("fib(10) = %d\n", fib(10)); return 0; } 

EMSCRIPTEN_KEEPALIVEマクロを次にEMSCRIPTEN_KEEPALIVEます。このマクロは2つのことを行います。 第一に、コードのどこでも使用されていなくても、コンパイラーによって関数がスローされるのを防ぎます。 第二に、外部呼び出しのために関数をエクスポートする必要があることを示しています。


コンパイルには、次のバッチファイルを使用します。


 SET EMSDKPATH=C:\bin\emsdk CALL %EMSDKPATH%\emsdk_env.bat emcc -O1 fib.c -o fib.html -fno-exceptions -fno-rtti 

ここで、 emccはEmscripten自体、 -O1最適化オプション、 fib.cコンパイルするもの、 -o fib.htmlコンパイルする場所、そして-fオプションで不要なものを無効にします。


出口で何を得る

コンパイルの出力で、コンパイルされたコードを実行するJavaScriptを含むHTMLファイル(fib.html)を取得します。


また、fib()関数とその呼び出しをmain()で見つけることができるfib.jsファイルも取得しました:


さらに、バイナリファイルfib.html.memが生成されます-これは「メモリイメージ」です。プログラムを開始する前のメモリは次のとおりです。ここにすべての定数とグローバル変数があります。


fib.htmlを開くと、次の画像が表示されます。



これはEmscriptenの標準の結果ビューです。 中央の黒い長方形は「コンソール」(stdout)の出力であり、特にprintf()がそこに表示されます。 上部の黒い長方形はキャンバスです。 Emscriptenは、あなたがそれを必要とするかどうかを知りませんが、念のためにここに作成します。


WebAssemblyでのコンパイル


WebAssemblyでコンパイルするために、ソースコードをC / C ++に変更する必要はありません(これは素晴らしいことです!)。


コンパイラー呼び出しのコマンドラインを少し変更するだけです。


 SET EMSDKPATH=C:\bin\emsdk CALL %EMSDKPATH%\emsdk_env.bat emcc -O1 fib.c -g -o fib.html -s WASM=1 -s NO_EXIT_RUNTIME=1 -s NO_FILESYSTEM=1 -fno-exceptions -fno-rtti --llvm-lto 1 

ここでの主な違いは、 -s WASM=1オプションの追加です。 残りの-s-f 、Emscriptenに不要なものを説明するために追加されます。 Emscriptenは非常に多くの処理を実行できるため、「万が一に備えて」、「突然必要になった」など、結果ファイルに多くのことを追加します。


コンパイルの結果として、fib.html、さらにfib.js(Emscriptenサービス関数のセット)、そして最後にfib.wasmも取得します。



WASMファイルの先頭にバイト00があり、次に文字「asm」があります。これらの最初の4バイトにより、エラーコードのあるスタブページではなく、wasmを読み込んでいることがわかります。 次の4バイトはバージョン番号で、この場合は1.0です。 メモリイメージ用の個別のファイルは生成されず、定数とグローバル変数は同じWASMファイルに含まれます。


ここでは結果のスクリーンショットを提供しません。asm.jsの例のように一対一に見えます。


ChromeでWebAssemblyをデバッグする

デバッグの観点から見てみましょう。 Chrome開発者ツール(F12)を開いた後、Sourcesに移動すると、新しいセクション「wasm」が表示されます。このセクションでは、ブロックのセットの中に関数を見つけ、そこにブレークポイントを置いて停止し、デバッガーに入ります


ご覧のとおり、前述のテキストビュー(WAST)はデバッグに使用されます。


次に、デバッグ情報を使用して同じコードをコンパイルします。このため、emccコマンドラインに-gオプションを追加します。 その結果、コンパイラーはfib.wastとfib.html.mapの2つのファイルを生成します。
テキスト表現ファイルfib.wastには、コードだけでなく、C / C ++のソースへの参照もあります。


  (func $_fib (param $0 i32) (result i32) (local $1 i32) ;; fib.c:11 (block $switch-default 

デバッグの観点からこれが何をもたらすかを見てみましょう。 [ソース]セクションのページを更新すると、ソースコードfib.cが表示され、ブレークポイントを設定、停止、ローカル変数を確認して、デバッガーとしてコードをステップ実行できます。


Emscriptenの機能


Emscriptenは2010年から開発を続けており、すでに多くのことを知っています。 この場合、私たちはもはやコンパイラーについてではなく、C / C ++コードから使用される一般的なライブラリーのサポートについて話します。 以下がサポートされています。



その他の機能:



より複雑な例


私は、C ++で書かれた古いソビエトのコンピューターのエミュレーターの私自身の趣味プロジェクトを持っています。 この記事で説明しまし 。 それ以来、なんとかそれを追加して、Qt(Windows / MacOS / Linux)に移植することができました。 そのため、さまざまなコンパイラーでビルドされたエミュレーションカーネル(〜280 KBのコード、〜7キロストリング)が既に割り当てられています。 実際、Emscriptenを使用してこのエミュレーターをコンパイルすることにより、WebAssemblyの学習を開始しました。 最初の打ち上げが成功するまで、仕事から2晩かかりましたが、これは良い結果であり、トピックを入力するためのしきい値が比較的低いことを示しています。 ほとんどの作業はJavaScriptに関連していました。JSからWASMメソッドを呼び出す方法、パラメーターを渡す方法、キャンバスを描画する方法などです。


ちなみに、エミュレートされたマシンの画面は、キャンバスに適した「ピクセル」形式で、メモリの固体ブロックの形で、完全にWASM内に形成されます。 JavaScriptの外部では、完成した「スクリーン」のアドレスのみが送信されます。 JavaScriptでは、このメモリブロックをキャンバスにコピーするだけです。


動作するエミュレータをここ見ることが できます 。まあ、 ソースコードも利用可能です


WASMとして構築されたエミュレーターのスクリーンショット


また、ある時点で、asm.jsの下でもこのエミュレータを収集するために、「図を完成させる」ことにしました。 私は自分でコーヒーを作り、そのために2、3時間の空き時間を取っておき、15分も経たないうちにエミュレーターがすでに動作し始めました。 予想外のようでした。 実際、生成されたHTMLファイルの違いを確認し、追加されたJavaScriptブロックを必要な場所に移動するだけで済みました。 唯一の違いは、asm.jsの場合、.memファイル、定数およびグローバル変数を含むメモリイメージをロードする必要があることです。 それ以外の場合、すべての呼び出しはまったく同じ方法で行われ、完成したページは見た目と動作がまったく同じでしたが、少し遅いかもしれません。


それで、Emscriptenを要約します。 同じコードから、asm.jsの形式とWebAssemblyの形式の結果を生成することを確認しました。得られた結果はまったく同じように見え、動作します(もちろん速度は例外です)。 実際の結果を得るためのエントリのしきい値は比較的低かった。


一方、Emscriptenはかなり洗練されたツールです。 コンパイル結果には、予期しない多くの情報が含まれていますが、これらは役に立つかもしれません。 したがって、小さなソースであっても、大量の結果コードが生成されます。 これらの一部はコマンドラインオプション無効にできますが 、一部は無効にできません。


Emscriptenが提供するものとWASMがすぐに使えるものとを区別することは非常に難しいため、EmscriptenでWebAssemblyの開発をすぐに開始する価値はないと思います。 しかし、実際のプロジェクトでは、Emscriptenは、まさに開発者に提供する機能により、非常に便利です。


WebAssemblyの現在の状態


2017年のWebAssemblyニュース:



ブラウザのサポート


2017年10月の初めに、状況は次のようになりました。



Edgeバージョンは、オペレーティングシステムのバージョンに関連付けられています。 Windows 10 Fall Creators Updateと一緒に、WebAssemblyがすぐに動作するEdge 16を取得します。設定に何も含める必要はありません。


WebAssemblyをサポートしないブラウザの場合、いわゆるを使用することになっています。 「Polyfill」、つまり、このブラウザーで実行できるコードへのWASMの自動変換。 特に、WebAssemblyを効果的にasm.jsに変換するプロトタイプが作成されました。 しかし、これまでのところ、このアプローチの実際の応用例は見ていません。


WebAssemblyの未来


WebAssemblyチームが現在取り組んでいる多くのこと:



性能


パフォーマンスの問題は実際には非常に複雑です。 WebAssemblyは、同じJavaScriptまたはasm.jsよりも常に高速であるとは限らないためです。 たとえば、 JavaScriptとWebAssemblyの簡単なベンチマークの比較をご覧ください。 最初のテスト、collisionDetectionでは、WASMがJSの88%を与えることがわかりました。 そして、次のテストでフィボナッチWASMはJSの3倍の、はるかに良い結果を生み出したとしましょう。


ここでは、速度に影響を与えるいくつかのポイントのみに注意しますが、実際には、もちろんもっと多くのポイントがあります。


WebAssemblyは、JS関数への集中的な呼び出しでパフォーマンスを大幅に失う可能性があることを既に述べました。


WebAssemblyは、メモリアクセスのパフォーマンスが低下します。このようなアピールのたびに、使用可能なメモリブロックの境界を超えているかどうかをチェックします。


WebAssemblyは、整数変数のタイプから大きな利益を得ることができます。 JSには、実際には常に64ビット浮動小数点であるNumber型のみがあり、整数は小数部のない浮動小数点数です。 JSエンジンでコンパイルする場合、整数には64ビット整数型が使用されます。 WASMでは、型のビット深度を選択します。32ビット整数型を使用する場合、64ビット整数よりも少し速い演算を行うと、計算速度が「不公平」になります。


一般に、「平均でWebAssemblyは10〜15%の速度向上を提供する」というようなものはなく、「平均」はないという意見がありました。 しかし、一般的に、集中的なコンピューティングの場合、WebAssemblyが多少なりとも目に見えるパフォーマンスの向上をもたらす可能性が最も高いと予測できます。 さらに、過去6か月間でさえ、ブラウザーの新しいバージョンのリリースによってWASMの速度がわずかに向上したことは明らかです。


おわりに


WebAssemblyを使用する



ユースケース


  1. 準備ができたら、今すぐWASMを使用してください
  2. 今すぐasm.jsを使用し、将来WASMにアップグレードします
  3. ブラウザに応じて、asm.jsまたはWASMを指定します

参照資料


このトピックに関する資料はたくさんありますが、ほとんどすべてが英語です。 ここで、私が最も役立つと思ういくつかのリンクを集めました。




Source: https://habr.com/ru/post/J342180/


All Articles