(注:このブログはもう更新していません)この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

[クラウド帳票エンジンDocurain]

Rhinoの最適化レベル毎の違いを正確に理解する


※Rhinoのバージョンは1.7R4です。


ウチの製品であるMOD99は、いくつかの箇所で式言語としてJavascriptを採用しています。
例えば帳票生成において、サーバーサイドで計算しなければならない導出値(代表例: 何らかの集約値)を出力したいときなど。
帳票をダウンロードさせる時に、セルの中にJSを書いておくと、サーバーサイドで実行された結果が入っていたりするような仕組みです。
まあ要するにサーバサイドJSということでございます。


で、いま開発しているシステムの帳票出力では、幾つかの項目でヘビーな集計を行なっており、JSの実行速度が問題になっておりました。
事実上オンラインバッチ相当の処理を、NoSQLっぽくJSで書いたりしているので、5分待ちとか。
いま、色々いじくって高速化しているところです。


さて、Java大好き人間の僕としては、JavascriptエンジンにはもちろんRhinoを使っているわけですが、こいつはJavascriptを実行するときに、内部的に最適化を行った上で実行する機能を持っています。
また、org.mozilla.javascript.Context#setOptimizationLevelで最適化レベルを指定できます。


Javadocには、

/**
 * Set the current optimization level.
 * <p>
 * The optimization level is expected to be an integer between -1 and
 * 9. Any negative values will be interpreted as -1, and any values
 * greater than 9 will be interpreted as 9.
 * An optimization level of -1 indicates that interpretive mode will
 * always be used. Levels 0 through 9 indicate that class files may
 * be generated. Higher optimization levels trade off compile time
 * performance for runtime performance.
 * The optimizer level can't be set greater than -1 if the optimizer
 * package doesn't exist at run time.
 * @param optimizationLevel an integer indicating the level of
 *        optimization to perform
 * @since 1.3
 *
 */
public final void setOptimizationLevel(int optimizationLevel);

とありまして、意訳は以下の通り。

現在の最適化レベルを設定する。

最適化レベルは-1から9までを指定可能であり、-1より小さい値は-1、9より大きい値は9であるとみなす。
最適化レベル-1は、常にインタプリタモードが用いられる。(つまり最適化なし)
最適化レベル0から9は、JavascriptからJavaクラス(バイトコード)へ変換された上で実行される。
最適化レベルの高さと(バイトコードへの)コンパイル時間はトレードオフである。
オプティマイザのパッケージが利用不可の場合、-1より大きい値を指定することはできない。

最適化レベルが高いほどに高度な最適化が行われる(トレードオフ)と言っております。

最適化レベル-1

最適化レベル-1は、要するに何もしないモードです。


org.mozilla.javascript.Contextの2414行目辺りのコードがこれ。

private Evaluator createCompiler()
{
    Evaluator result = null;
    if (optimizationLevel >= 0 && codegenClass != null) {
        result = (Evaluator)Kit.newInstanceOrNull(codegenClass);
    }
    if (result == null) {
        result = createInterpreter();
    }
    return result;
}


まあ見て分かる通り、最適化レベルが-1以下ならばcreateInterpreterが呼ばれます。
評価器(Evaluator)がインタプリタだってことです。

最適化レベル1以上

まず見るべきは、org.mozilla.javascript.optimizer.Codegenです。
バイトコードを生成している、最適化の本丸です。


このクラスで最適化レベルに関係している箇所は以下の様な感じでして、見どころが3箇所ほどあります。

private void transform(ScriptNode tree)
{
    initOptFunctions_r(tree);

    int optLevel = compilerEnv.getOptimizationLevel();

    Map<String,OptFunctionNode> possibleDirectCalls = null;
    if (optLevel > 0) { // 見どころ1
       /*
        * Collect all of the contained functions into a hashtable
        * so that the call optimizer can access the class name & parameter
        * count for any call it encounters
        */
        if (tree.getType() == Token.SCRIPT) {
            int functionCount = tree.getFunctionCount();
            for (int i = 0; i != functionCount; ++i) {
                OptFunctionNode ofn = OptFunctionNode.get(tree, i);
                if (ofn.fnode.getFunctionType()
                    == FunctionNode.FUNCTION_STATEMENT)
                {
                    String name = ofn.fnode.getName();
                    if (name.length() != 0) {
                        if (possibleDirectCalls == null) {
                            possibleDirectCalls = new HashMap<String,OptFunctionNode>();
                        }
                        possibleDirectCalls.put(name, ofn);
                    }
                }
            }
        }
    }

    if (possibleDirectCalls != null) {
        directCallTargets = new ObjArray();
    }

    // 見どころ2
    OptTransformer ot = new OptTransformer(possibleDirectCalls,
                                           directCallTargets);
    ot.transform(tree);

    if (optLevel > 0) { // 見どころ3
        (new Optimizer()).optimize(tree);
    }
}
見どころ1,2: 関数呼び出しの最適化

"見どころ1"と書いてあるコード近くのコメントによると
「(抽象構文木に含まれる)すべての関数をハッシュテーブルに集めて、オプティマイザが(関数名, パラメータ数)のキーでダイレクトにアクセスできるようにする」
だそうです。

見どころ1のあたりで構文木をトラバースして関数を集め、見どころ2のあたりで構文木の最適化を行なっています。

で、生成されたバイトコードは、関数呼び出しをorg.mozilla.javascript.optimizer.OptRuntimeクラスのcall0, call1, call2...というメソッドへフォワードしているようです。1や2は引数の数を表しています。



見どころ3: Optimizer

Optimizerクラスで、抽象構文木の構造を最適化しているようです。
具体的には、Optimizer#rewriteForNumberVariablesの中で、『数値として評価された式』の2項演算を最適化しているようです。
コレ以上の解説は面倒なので割愛。


その他

その他にContext#getOptimizationLevel()を使っている箇所を検索すると、ScriptRuntime#initStandardObjectsが見つかります。


コードの抜粋は以下。

public static ScriptableObject initStandardObjects(Context cx, ScriptableObject scope, boolean sealed) {
    if (scope == null) {
        scope = new NativeObject();
    }
    scope.associateValue(LIBRARY_SCOPE_KEY, scope);
    (new ClassCache()).associate(scope);

    BaseFunction.init(scope, sealed);
    NativeObject.init(scope, sealed);

    Scriptable objectProto = ScriptableObject.getObjectPrototype(scope);

    ...(略)

    NativeArray.init(scope, sealed);
    if (cx.getOptimizationLevel() > 0) {
        // When optimizing, attempt to fulfill all requests for new Array(N)
        // with a higher threshold before switching to a sparse
        // representation
        NativeArray.setMaximumInitialCapacity(200000);
    }

    NativeString.init(scope, sealed);
    NativeBoolean.init(scope, sealed);
    NativeNumber.init(scope, sealed);
    NativeDate.init(scope, sealed);
    NativeMath.init(scope, sealed);
    NativeJSON.init(scope, sealed);

    NativeWith.init(scope, sealed);
    NativeCall.init(scope, sealed);
    NativeScript.init(scope, sealed);

    ...(略)

    return scope;
}

長いので省略してますが、注目すべきは

NativeArray.init(scope, sealed);
if (cx.getOptimizationLevel() > 0) {
    // When optimizing, attempt to fulfill all requests for new Array(N)
    // with a higher threshold before switching to a sparse
    // representation
    NativeArray.setMaximumInitialCapacity(200000);
}

です。NativeArray#setMaximumInitialCapacityを呼ぶことで、NativeArrayのコンストラクタの動作が変わります。

public NativeArray(long lengthArg)
{
    denseOnly = lengthArg <= maximumInitialCapacity;
    if (denseOnly) {
        int intLength = (int) lengthArg;
        if (intLength < DEFAULT_INITIAL_CAPACITY)
            intLength = DEFAULT_INITIAL_CAPACITY;
        dense = new Object[intLength];
        Arrays.fill(dense, Scriptable.NOT_FOUND);
    }
    length = lengthArg;
}

にある通り、denseというフィールドの扱いが変わるようです。


じゃあdenseとは何やねん、ですが

/**
 * Fast storage for dense arrays. Sparse arrays will use the superclass's
 * hashtable storage scheme.
 */
private Object[] dense;

にある通り、インデックスのみで要素にアクセスするような『純粋な配列』の場合に、ハッシュ構造のデータストレージを使うよりも高速に処理できるようにするための、一種の最適化のようです。
maximumInitialCapacityの初期値は10000なので、最適化レベルを1以上にする場合は、より積極的にdenseを使おうとするという挙動になります。
すなわち、巨大な配列(上限要素数20万)でもdenseを使おうとするわけです。


ま、要素数10000を超える配列を頻繁に使う場合は少し処理が早くなるのでは。試してませんが。。。

最適化レベル0

さて、これまで見た通り、どうやら最適化レベル1以上は意味が無い(何を指定しても同じ)ように見えます。
じゃあレベル0はどないやねんという話ですが、上で述べたorg.mozilla.javascript.Context#createCompilerでインタプリタを使わないという箇所のみが変わるようです。

まとめ

  • 最適化レベル-1以下
  • 最適化レベル0
  • 最適化レベル1以上
    • バイトコードが生成される
    • 構文木を変えるような最適化も有り
    • 巨大な配列の処理が少し早くなる(かも)
    • 1から9まで何を指定しても結果は同じ。

です。間違いがあるようなら教えて下さいませ。

おまけ

実行に5分かかるJavascriptの最適化レベルを9から0にして再実行してみましたが、依然として5分のままでした。
レベルを1以上にするのは、特殊な事情がない限りそれほど意味が無いかもしれません。