ホーム
» コードの最適化
一般的に、最初に最適化なしでコードをコンパイルして実行する必要があります。これは、最適化レベルを指定せずにemcc
を実行した場合のデフォルトです。このような最適化されていないビルドには、コードが正しく実行されることを確認するのに非常に役立つチェックとアサーションが含まれています。コードが正しく実行されたら、いくつかの理由から、配布するビルドを最適化することを強くお勧めします。まず、最適化されたビルドははるかに小さく高速であるため、迅速にロードされ、よりスムーズに実行されます。第二に、**非**最適化ビルドには、ファイル名や関数名、JavaScript内のコードコメントなど、デバッグ情報が含まれています(サイズは増加するだけでなく、ユーザーに配布したくないものも含まれる可能性があります)。
このページの残りの部分では、コードを最適化する方法について説明します。
最適化フラグをemcc実行時に指定することで、コードを最適化します。レベルには、-O0(最適化なし)、-O1、-O2、-Os、-Oz、-Og、および-O3が含まれます。
たとえば、最適化レベル-O2
でコンパイルするには
emcc -O2 file.cpp
より高い最適化レベルでは、より積極的な最適化が導入され、コンパイル時間の増加を犠牲にして、パフォーマンスとコードサイズの向上が得られます。レベルは、コード内の未定義動作に関連するさまざまな問題も強調表示できます。
使用する最適化レベルは、主に開発の現在の段階によって異なります。
コードを最初に移植する際には、デフォルト設定(最適化なし)を使用してコードで *emcc* を実行します。コードが機能し、デバッグして問題を修正してから続行します。
開発中は、コンパイル/テストの繰り返しサイクルを短縮するために、より低い最適化レベルでビルドします(-O0
または-O1
)。
-O2
でビルドして、最適化されたビルドを取得します。
-O3
または-Os
でビルドすると、-O2
よりもさらに優れたビルドを作成でき、リリースビルドで検討する価値があります。-O3
ビルドは-O2
よりもさらに最適化されていますが、コンパイル時間が大幅に増加し、コードサイズが大きくなる可能性があります。-Os
はコンパイル時間の増加において似ていますが、追加の最適化を行いながらコードサイズ削減に重点を置いています。これらのさまざまな最適化オプションを試して、アプリケーションに最適なものを確認することをお勧めします。
その他の最適化については、次のセクションで説明します。
注記
*emcc* の最適化フラグ(-O1, -O2
など)の意味は、 *gcc*、 *clang*、およびその他のコンパイラと似ていますが、WebAssemblyの最適化には追加の最適化タイプが含まれるため、異なる部分もあります。 *emcc* レベルからLLVMビットコード最適化レベルへのマッピングは、リファレンスに記載されています。
ソースファイルをオブジェクトファイルにコンパイルする方法は、clangとLLVMを使用するネイティブビルドシステムと同様に機能します。オブジェクトファイルを最終実行ファイルにリンクする際に、Emscriptenは最適化レベルに応じて追加の最適化も行います。
Binaryenオプティマイザが実行されます。Binaryenは、LLVMが行わないWasmに対する汎用的な最適化と、プログラム全体の最適化も行います。(Binaryenのプログラム全体の最適化は、インライン化などを行う可能性があり、LLVM IR属性(noinline
など)は既に失われているため、場合によっては驚くべき結果になる可能性があります。)
この段階でJavaScriptが生成され、EmscriptenのJSオプティマイザによって最適化されます。必要に応じて、Closureコンパイラを実行することもできます。これは、コードサイズを削減するために強くお勧めします。
Emscriptenは、WasmとJSを組み合わせたものを最適化し、インポートとエクスポートを最小化し、メタDCEを実行して、2つの世界にまたがるサイクル内の未使用コードを削除します。
リンク時の追加の最適化作業をスキップするには、-O0
または-O1
でリンクします。これらのモードでは、Emscriptenはより高速な反復時間を重視します。(ソースファイルが異なる最適化レベルでコンパイルされていた場合でも、これらのフラグを使用してリンクしても問題ありません。)
リンク時の非最適化作業もスキップするには、-sWASM_BIGINT
でリンクします。BigIntサポートを有効にすると、EmscriptenがJS/Wasm境界でi64
値を処理するためにWasmを「合法化」する必要がなくなります(BigIntを使用するとi64
値は合法であり、追加の処理は必要ありません)。
いくつかのリンクフラグは、リンク段階で追加の作業を追加するため、速度を低下させる可能性があります。たとえば、-g
はDWARFサポートを有効にし、-sSAFE_HEAP
のようなフラグはJSの後処理を必要とし、-sASYNCIFY
のようなフラグはWasmの後処理を必要とします。フラグによってWasmがwasm-ld
後に変更されないように、可能な限り高速なリンクを確保するには、-sERROR_ON_WASM_CHANGES_AFTER_LINK
でビルドします。このオプションを使用すると、EmscriptenがWasmを変更する必要がある場合、リンク中にエラーが発生します。たとえば、-sWASM_BIGINT
を渡さなかった場合、合法化によってWasmの変更が強制されることが通知されます。-O2
以上でビルドした場合もエラーが発生します。これは、通常Binaryenオプティマイザが実行されるためです。
コード生成に影響を与えるいくつかのフラグをコンパイラに渡すことができ、パフォーマンスにも影響します(たとえば、DISABLE_EXCEPTION_CATCHINGなど)。これらはsrc/settings.jsに記載されています。
Emscripten はデフォルトで WebAssembly を出力します。 -sWASM=0
を使用してこれをオフにすることができます(この場合、Emscripten は JavaScript を出力します)。これは、Wasm がまだサポートされていない環境で出力を実行する場合に必要ですが、コードが大きくなり、遅くなるという欠点があります。
このセクションでは、コードサイズに関連する最適化と問題について説明します。これらは、可能な限り小さなフットプリントが必要な小規模なプロジェクトやライブラリ、およびサイズが大きいために問題(起動速度の低下など)を引き起こす可能性のある大規模なプロジェクトの両方で役立ちます。
パフォーマンスにそれほど影響を受けないソースファイルは、プロジェクト内で-Osまたは-Ozを使用してビルドし、残りのファイルは-O2を使用することをお勧めします(-Osと-Ozは-O2に似ていますが、パフォーマンスを犠牲にしてコードサイズを削減します。-Ozは-Osよりもコードサイズを削減します)。
個別に、最終的なリンク/ビルドコマンドを-Os
または-Oz
を使用して実行し、コンパイラがWebAssemblyモジュールの生成時にコードサイズを優先するようにすることができます。
上記に加えて、以下のヒントはコードサイズの削減に役立ちます。
コンパイルされていないコードにクロージャコンパイラを使用します。--closure 1
。これにより、サポートJavaScriptコードのサイズを大幅に削減でき、強くお勧めします。ただし、独自の追加JavaScriptコード(たとえば--pre-js
内)を追加する場合は、クロージャアノテーションを適切に使用する必要があります。
このトピックに関するFlohのブログ投稿は非常に役立ちます。
すべてのブラウザでサポートされているWebサーバーでgzip圧縮を使用してください。
次のコンパイラ設定が役立ちます(詳細についてはsrc/settings.js
を参照してください)。
-sINLINING_LIMIT
を使用して、可能な限りインライン化を無効にします。-Osまたは-Ozでコンパイルすると、一般的にインライン化も回避されます。(ただし、インライン化によってコードが高速化される場合もあるため、注意深く使用してください)。
-sFILESYSTEM=0
オプションを使用して、ファイルシステムサポートコードのバンドルを無効にすることができます(使用されていない場合はコンパイラが最適化しますが、常に成功するとは限りません)。これは、たとえば純粋な計算ライブラリをビルドする場合に役立ちます。
ENVIRONMENT
フラグを使用すると、出力がWeb上でのみ実行されるか、node.jsでのみ実行されるかなどを指定できます。これにより、コンパイラはすべての可能な実行時環境をサポートするコードを出力しなくなるため、約2KB削減できます。
リンク時最適化(LTO)を使用すると、コンパイラはより多くの最適化を実行できます。これは、個別のコンパイルユニット全体、さらにはシステムライブラリ全体でインライン化できるためです。LTOは、-flto
を使用してオブジェクトファイルをコンパイルすることで有効になります。このフラグの効果は、LTOオブジェクトファイルを出力することです(技術的には、ビットコードを出力することを意味します)。リンカは、WasmオブジェクトファイルとLTOオブジェクトファイルの混合を処理できます。-flto
をリンク時に渡すと、LTOシステムライブラリも使用されます。
したがって、LLVM Wasmバックエンドで最大のLTOの機会を可能にするには、すべてのソースファイルを-flto
でビルドし、flto
でリンクします。
-sEVAL_CTORS
でビルドすると、コンパイル時に可能な限り多くのコードが評価されます。「グローバルコンストラクタ」関数(main()
の前に実行されるLLVMが出力する関数)とmain()
自体が含まれます。評価できるものはすべて評価され、その結果の状態がWasmに「スナップショット」されます。その後、プログラムが実行されると、その状態から開始され、そのコードを実行する必要がないため、時間を節約できます。
この最適化によって、コードサイズが減少する場合も増加する場合もあります。たとえば、少量のコードがメモリに多くの変更を生成する場合、全体的なサイズが増加する可能性があります。このフラグを使用してビルドし、コードサイズと起動速度を測定して、プログラムでトレードオフが価値があるかどうかを確認することをお勧めします。
できるだけ多くの評価できないものを延期することで、EVAL_CTORSに適したコードを作成できます。たとえば、インポートへの呼び出しはこの最適化を停止するため、GLコンテキストを作成してから、メモリ内の関連のないデータ構造を設定するための純粋な計算を行うゲームエンジンがある場合、その順序を逆にすることができます。その後、純粋な計算を最初に実行して評価し、インポートへのGLコンテキスト作成呼び出しによってそれが妨げられることはありません。その他にも、argc/argv
を使用しない、getenv()
を使用しないなどを行うことができます。
このオプションを使用すると、ログが表示され、改善できるかどうかを確認できます。emcc -sEVAL_CTORS
からの出力例を以下に示します。
trying to eval __wasm_call_ctors
...partial evalling successful, but stopping since could not eval: call import: wasi_snapshot_preview1.environ_sizes_get
recommendation: consider --ignore-external-input
...stopping
最初の行は、グローバルコンストラクタを実行するLLVMの関数の評価を試みたことを示しています。関数のいくつかを評価しましたが、WASIインポートenviron_sizes_get
で停止しました。これは、環境から読み取ろうとしていることを意味します。出力にあるように、EVAL_CTORS
に外部入力を無視するように指示できます。これは、そのようなものを無視します。モード2
で有効にできます。つまり、emcc -sEVAL_CTORS=2
でビルドします。
trying to eval __wasm_call_ctors
...success on __wasm_call_ctors.
trying to eval main
...stopping (in block) since could not eval: call import: wasi_snapshot_preview1.fd_write
...stopping
これで、__wasm_call_ctors
を完全に評価することに成功しました。次にmain
に進み、WASIのfd_write
(つまり、何かを出力する呼び出し)への呼び出しのために停止しました。
コードサイズを削減するための前のセクションは、非常に大規模なコードベースでも役立ちます。さらに、以下は役立つ可能性のあるその他のトピックです。
ブラウザでメモリ制限に達した場合、他のコンテンツを含むWebページ内ではなく、プロジェクトを単独で実行すると役立つ場合があります。プロジェクトを含む新しいWebページ(新しいタブまたは新しいウィンドウとして)を開くと、メモリ断片化の問題を回避する可能性が高まります。
モジュールが大きすぎて、ダウンロードとインスタンス化にかかる時間がアプリケーションの起動パフォーマンスに著しく影響する場合、モジュールを分割し、アプリケーションの起動に必要ないコードの読み込みを遅らせる価値があります。これを行う方法については、モジュール分割を参照してください。モジュール分割は実験的な機能であり、変更される可能性があります。
C++例外のキャッチ(具体的には、キャッチブロックの出力)は、-O1
(およびそれ以上)ではデフォルトでオフになっています。WebAssemblyが現在例外を実装する方法により、これによりコードがはるかに小さくなり、高速になります(最終的には、Wasmは例外に対するネイティブサポートを獲得し、この問題は発生しなくなります)。
最適化されたコードで例外を再度有効にするには、-sDISABLE_EXCEPTION_CATCHING=0
を使用してemccを実行します(src/settings.jsを参照)。
注記
例外のキャッチが無効になっている場合、スローされた例外はアプリケーションを終了します。つまり、例外は依然としてスローされますが、キャッチされません。
注記
キャッチブロックが出力されない場合でも、ソースファイルを-fno-exceptions
でビルドしない限り、コードサイズに追加のオーバーヘッドがあります。これにより、すべての例外サポートコードが省略されます(たとえば、std::vectorのエラーで適切なC++例外オブジェクトを作成せず、発生した場合はアプリケーションを中止します)。
C++ランタイム型情報サポート(動的キャストなど)は、不要な場合もあるオーバーヘッドを追加します。たとえば、Box2Dでは、rttiも例外も必要なく、ソースファイルを-fno-rtti -fno-exceptions
でビルドすると、出力が15%も縮小されます(!)。
-sALLOW_MEMORY_GROWTH
でビルドすると、アプリケーションの要求に応じて使用されるメモリの合計量を変更できます。これは、事前に必要な量がわからないアプリケーションに役立ちます。
デバッグモード(EMCC_DEBUG)を有効にして、主要な最適化操作を含む各コンパイルフェーズのファイルを出力します。
デフォルトで使用されるmalloc/free
の実装はdlmalloc
です。emmalloc
(-sMALLOC=emmalloc
) を選択することもできます。これはサイズが小さいですが速度は遅くなります。また、mimalloc
(-sMALLOC=mimalloc
) はサイズが大きくなりますが、malloc/free
での競合があるマルチスレッドアプリケーションではスケーラビリティが向上します(メモリ確保パフォーマンスを参照)。
試したい安全でない最適化をいくつか紹介します。
--closure 1
: これは、生成されない(サポート/Glue)JSコードのサイズを削減し、起動時間を短縮するのに役立ちます。ただし、適切なClosure Compilerアノテーションとエクスポートを行わないと、動作が不安定になる可能性があります。それでも、試してみる価値はあります!
最新のブラウザには、コードの遅い部分を特定するのに役立つJavaScriptプロファイラが搭載されています。各ブラウザのプロファイラには制限があるため、複数のブラウザでプロファイリングすることを強くお勧めします。
コンパイルされたコードにプロファイリングに必要な情報が含まれていることを確認するには、プロファイリングと最適化、その他のフラグを使用してプロジェクトをビルドしてください。
emcc -O2 --profiling file.cpp
Emscriptenでコンパイルされたコードは、多くの場合、ネイティブビルドに近い速度になります。パフォーマンスが予想よりも大幅に低い場合は、以下の追加のトラブルシューティング手順を実行することもできます。
プロジェクトのビルドは、ソースコードファイルをLLVMにコンパイルすると、LLVMからJavaScriptを生成する2段階のプロセスです。両方の手順で同じ最適化値(-O2
または-O3
)を使用してビルドしましたか?
複数のブラウザでテストします。あるブラウザではパフォーマンスが許容範囲内だが、別のブラウザでは大幅に低い場合は、問題のあるブラウザとその他の関連情報を記載してバグレポートを提出してください。