原文はこちら。
https://blogs.oracle.com/nashorn/entry/improving_nashorn_startup_time_using
Nashornはウォームアップが済めば、通常Rhinoよりも速いのですが、その起動時ならびにウォームアップの挙動はRhinoに比べて些か悪いことがあります。これには様々な理由がありますが、これにはinvokedynamic callsitesを実行時にブートする必要があるだけでなく、JDK 8やNashorn自身のinvokedynamic実装が効率的でないことも理由としてあります。
JVM側とNashornの両面で改善の努力をすすめていますが、起動時のオーバーヘッドを削減するため、キャッシュを実装し、コンパイル済みのJavaScriptコードを再利用する機能をNashornに追加しました。これらの機能は、8u20から追加されはじめ、先頃リリースされた8u40リリースで強化されています。
このエントリでは、JDK 8u20でスクリプトのロードのスピードアップのためのいくつかの方法をご紹介します。サンプルスクリプトとしてlodashライブラリを使用します。lodashを含め、この記事で使用するサンプルコードは、
こちらからダウンロードできます。
lodash
https://lodash.com/
Sharing compiled scripts between Threads
JDK 8リリース当初、NashornがJavaのバイトコードにコンパイルしたJavaScriptコードはシングルスレッドでかつ単一のJavaScript環境でのみ利用することができました。これはつまり、同じスクリプトを複数のJavaScript環境で実行するためには、その都度最初からコンパイルする必要があった、ということです。
JDK 8u20から、コンパイル済みのスクリプトクラスを複数のグローバルオブジェクト、複数のスレッドで同時に使うことができるようになりました。単一のScriptEngineを複数のバインディングやスレッドで利用することでこの機能を利用できます。
以下のコードは、新たなスコープ・バインディングでスクリプトを実行します。スクリプトエンジンとスクリプトのURLを引数として取得し、スクリプトを(NashornスクリプトエンジンAPIのBindingとして表される)新たなグローバルオブジェクトで実行します。
private Object evaluateInNewScope(ScriptEngine engine, URL url) throws Exception {
Bindings b = engine.createBindings();
ScriptContext context = new SimpleScriptContext();
context.setBindings(b, ScriptContext.ENGINE_SCOPE);
return engine.eval(new URLReader(url), context);
}
クラスキャッシュのサンプルコードを実行して動作を確認できます。このクラスは複数のJavaScriptスコープでunderscore.jsというJavaScriptライブラリをロードし、実行します。初期のJDK8とJDK 8u20以後の両方でdemoを実行すると、後者の環境でのスクリプト実行のほうがずっと高速であることがわかります。
この機能は素敵なことに、マルチスレッド環境でも利用できます。
マルチスレッドでのクラスキャッシュのサンプルコードを実行して確認できます。異なるJavaScriptスコープバインディングを使うだけでなく、異なるスレッドでスクリプトを実行しています。再度言いますが、初期のJDK 8と比べてJDK 8u20以後で実行するほうがずっと高速です。
-ccs
や
--class-cache-size
オプションを使って、内部クラスキャッシュのサイズを設定できます。デフォルト値は50で、これはScriptEngineがおよそ50個のコンパイル済みスクリプトクラスをキャッシュする、という意味です。もっと多くのスクリプトを使う場合には、この設定値を大きくする必要があります。
Persisting compiled classes to disk
内部クラスキャッシュのおかげでNashornエンジン内で同じスクリプトを繰り返し利用する回数を減らすことができますが、スクリプトの初期コンパイルに関連するオーバーヘッドがまだかなり存在します。それゆえ、後の実行でスクリプトの再コンパイルをしなくてすむよう、JDK 8u20ではコンパイル済みのNashornクラスをディスクに格納する機能が導入されました。
-pcc
もしくは
--persistent-code-cache
オプションを使って、永続クラスキャッシュ(Persistent class caching)を有効化することができます。このオプションはNashornコマンドラインツール実行時にjjsに渡すか、もしくは
NashornScriptEngineFactory
getScriptEngine
メソッドに以下のように渡すことができます。
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine("-pcc");
デフォルトでは、Nashornクラスは現在のワーキング・ディレクトリの
nashorn_code_cache
というディレクトリに格納されます。この場所は
nashorn.persistent.code.cache
というシステムプロパティを使ってオーバーライドすることができます。注意頂きたいのは、このディレクトリ内のファイルは純粋なJavaクラスではない、ということです。それは、ディスクからコンパイルされたスクリプトを復元するために、クラスのバイトコードに加えて、様々なメタデータを格納する必要があるためです。
永続クラスキャッシュのサンプルコードを使ってテストすることができます。このクラスはlodashライブラリを永続コードキャッシュを使って実行します。コンパイル済みクラスをディスクから読み出すことによる改善が50%弱で、いささか期待外れと感じる方がいらっしゃるかもしれません。
この理由は、コンパイル済みスクリプトの最初の実行の大部分が実際のところコンパイルのオーバーヘッドではなく、コンパイル済みクラスのロードおよびinvokedynamic callsiteの起動であるためです。繰り返し同じスクリプトをNashornで実行し、できる限りオーバーヘッドを少なくしたいというのであれば、これは有用な選択肢と言えるでしょう。
Persisting optimistic type info
JDK 8u40では、Optimistic Typeという新機能も導入されています。このOptimistic Typesは8u40ではデフォルトでは無効になっており、明示的に
-ot
もしくは
--optimistic-types
オプションを使って有効化する必要があります。
-ot=true
--optimistic-types=true
Optimistic TypesはJavaScriptプログラムのデータ型を仮定するというもので、この仮定が間違っている場合には、実行中に再コンパイルします。この機能により、JavaScriptプログラムの最終的な速度はかなり向上しますが、起動およびウォームアップのパフォーマンスに追加の負担がかかります。それゆえ、キャッシュとバイトコードの再利用がより一層有用になるのです。
幸運にも、Optimistic Typeと前述の永続コードキャッシュを組み合わせて利用できます。関数の再コンパイルにより、その関数の以前のバージョンは、永続コードキャッシュで上書きされますので、永続キャッシュをその後の実行で使用する場合、関数の「正しい」バージョンがすぐにロードされるようになります。これにより、実行中の再コンパイルの必要性が減り、Optimistic Typeがない場合に比べてより一層メリットを得られます。
しかし、多くのアプリケーションにとって過剰である場合があるため、コンパイル済みクラス全体をディスクに永続化しなくても、このソリューションが改善される可能性があります。本当に保存が必要なのは、コンパイルされたスクリプト内のcallsitesの型情報だけだからです。
JDK 8u40では、スクリプトの型情報だけをディスクに保存し、その後の同一スクリプトの実行で再利用できる機能も導入しています。起動時間の改善という点では、先ほどのコンパイル済みクラス全体を永続化する場合に類似していますが、型情報だけを保存しているため、結果としてディスクの使用量が減っています。
型情報のキャッシュとOptimistic Typeの組み合わせを
サンプルコードで実際に確認することができます。Optimistic Type情報はシステムディレクトリに格納されます。このシステムディレクトリはプラットフォーム依存です。この機能およびその他の様相は様々なシステムプロパティを使って調整できます。詳しくは、NashornのDeveloper Readmeファイルをご覧下さい。
DEVELOPER_README
http://hg.openjdk.java.net/jdk8u/jdk8u-dev/nashorn/file/tip/docs/DEVELOPER_README
まとめ
JDK 8u20と8u40で、Nashornのスタートアップならびにウォームアップの性能を改善するための様々な新機能が導入されています。
これらの機能だけでNashornの起動とウォームアップの問題がすべて解決するとは思っていませんが、NashornおよびJDK 8での体験が改善することを期待しています。引き続き様々なレベルでパフォーマンス向上の取り組みを続けています。
これらの機能に関する質問がありましたら、お気軽にnashorn-devメーリングリストにどうぞ。