JVMメソッドインライン

8517 ワード

IntelliJ IDEAではCtrl+Alt+Mを用いて方法を分割する.コードを選択してこの組み合わせを叩くのは簡単です.Eclipseも同様のショートカットキーを使用します.私は長い方法が嫌いです.これは次の方法といえば長すぎると思います.
public void processOnEndOfDay(Contract c) {
    if (DateUtils.addDays(c.getCreated(), 7).before(new Date())) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}

まず、可読性が悪いと判断する条件があります.それがどのように実現されても、それが何をするかが最も重要です.まず分割しましょう
public void processOnEndOfDay(Contract c) {
    if (isOutdated(c)) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}
private boolean isOutdated(Contract c) {
    return DateUtils.addDays(c.getCreated(), 7).before(new Date());
}

明らかにこの方法はここに置くべきではないF 6転送例の方法を押すべきではない.
public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}

私のIDEはisOutdatedメソッドをContractのインスタンスメソッドに変更しました.これでいいですね.しかし、私はまだ気分が悪い.この方法はやることが雑すぎる.ブランチは、ビジネス関連の論理priorityHandlingを処理し、システム通知およびログを送信します.もう1つのブランチは,判断条件に基づいてシステム通知を行いながらログを記録する.私たちはまず期限切れの契約を処理することを独立した方法に分割します.
public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        handleOutdated(c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}
private void handleOutdated(Contract c) {
    priorityHandling(c, OUTDATED_FEE);
    notifyOutdated(c);
    log.info("Outdated: {}", c);
}

これで十分だと思う人もいますが、2つの分岐は非対称で目立つと思います.handleOutdatedメソッドはレベルが高く、elseブランチは詳細に偏っています.ソフトウェアは明確で読みやすいので、異なる階層間のコードを混同しないでください.これでもっと満足します
public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        handleOutdated(c);
    } else {
        stillPending(c);
    }
}
private void handleOutdated(Contract c) {
    priorityHandling(c, OUTDATED_FEE);
    notifyOutdated(c);
    log.info("Outdated: {}", c);
}
private void stillPending(Contract c) {
    if(sendNotifications) {
        notifyPending(c);
    }
    log.debug("Pending {}", c);
}

この例は少し装っているように見えますが、実は私が証明したいのは別のことです.今ではあまり見かけませんが、このような場合、運用効率に影響を与えるのではないかと心配する開発者もいます.彼らはJVMが実はとても素晴らしいソフトウェアであることを知らないで、それは実はJava言語を振っていくつかの街を振って、それの中には多くの驚くべき実行時の最適化が建てられています.まず短い方法はJVM推定に有利である.プロセスのより顕著な役割ドメインのより短い副作用もより顕著である.長い方法であればJVMは直接ひざまずいたかもしれません.2つ目の理由はもっと重要です
メソッドインライン
JVMがいくつかの小さなメソッドが頻繁に実行されていることを監視すると、メソッドの呼び出しがメソッドボディ自体に置き換えられます.例えば次のような
private int add4(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
    return x1 + x2;
}

一定時間実行すると、JVMはadd 2メソッドを削除し、コードを翻訳します.
private int add4(int x1, int x2, int x3, int x4) {
    return x1 + x2 + x3 + x4;
}

コンパイラではなくJVMということに注意してください.JAvacはバイトコードを生成する際に保守的なこれらの作業をJVMに投げつける.このような設計決定は非常に賢明であることが明らかになった.
JVMがより明確に動作するターゲット環境CPUメモリアーキテクチャは、より積極的に最適化することができます.
JVMは、どのメソッドが頻繁に実行されているか、どの虚メソッドが1つしか実装されていないかなど、コード実行時の特徴を発見することができます.
古いコンパイラでコンパイルされたclassは、新しいバージョンのJVMでより高速な実行速度を得ることができます.JVMの更新とソースコードの再コンパイルは、後者に傾いているに違いありません.
私たちはこれらの仮説をテストします.私は小さなプログラムを書いた.それは分治原則の最悪の実現の称号を持っている.add 128メソッドは128個のパラメータを必要とし、add 64メソッドを2回呼び出した(前後2回ずつ).add 64も同様であるが、add 32が2回呼び出された.あなたの推測は間違いなく最後にadd 2の方法でこのすべてを終わらせます.それは苦労して働いています.君の目が見えなくならないように省略した数字がある
public class ConcreteAdder {
    public int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
        return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
                add64(x65, x66, x67, x68, ... more ..., x127, x128);
    }
    private int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
        return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
                add32(x33, x34, x35, x36, ... more ..., x63, x64);
    }
    private int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32) {
        return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
                add16(x17, x18, x19, x20, ... more ..., x31, x32);
    }
    private int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16) {
        return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
    }
    private int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
        return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
    }
    private int add4(int x1, int x2, int x3, int x4) {
        return add2(x1, x2) + add2(x3, x4);
    }
    private int add2(int x1, int x2) {
        return x1 + x2;
    }
}

呼び出しadd 128メソッドは、最終的に127のメソッド呼び出しを生成することが容易である.多すぎます.参考までに、以下に簡単な直接的な実装バージョンがあります.
public class InlineAdder {
    public int add128n(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
        return x1 + x2 + x3 + x4 + ... more ... + x127 + x128;
    }
}

最後に抽象クラスと継承を使用した実装バージョンをもう1つ使用します.127個のダミーメソッド呼び出しのオーバーヘッドは非常に大きい.これらの方法は動的に配布する必要があるため、より高い要求があるため、インラインできません.
public abstract class Adder {
    public abstract int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128);
    public abstract int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64);
    public abstract int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32);
    public abstract int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16);
    public abstract int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8);
    public abstract int add4(int x1, int x2, int x3, int x4);
    public abstract int add2(int x1, int x2);
}

もう一つの実現
public class VirtualAdder extends Adder {
    @Override
    public int add128(int x1, int x2, int x3, int x4, ... more ..., int x128) {
        return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
                add64(x65, x66, x67, x68, ... more ..., x127, x128);
    }
    @Override
    public int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
        return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
                add32(x33, x34, x35, x36, ... more ..., x63, x64);
    }
    @Override
    public int add32(int x1, int x2, int x3, int x4, ... more ..., int x32) {
        return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
                add16(x17, x18, x19, x20, ... more ..., x31, x32);
    }
    @Override
    public int add16(int x1, int x2, int x3, int x4, ... more ..., int x16) {
        return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
    }
    @Override
    public int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
        return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
    }
    @Override
    public int add4(int x1, int x2, int x3, int x4) {
        return add2(x1, x2) + add2(x3, x4);
    }
    @Override
    public int add2(int x1, int x2) {
        return x1 + x2;
    }
}

私のもう一つの@Cacheable負荷に関する文章の熱心な読者の励ましを受けて、私はこの2つの過度に分割されたConcreteAdderとVirtualAdderの負荷を比較するために簡単な基準テストを書いた.結果は意外にも少し頭がつかめない.私は2台の機械の上で赤色と青色の同じプログラムをテストして異なっているのは2台目の機械のCPUコア数がもっと多くて64ビットです
具体的な環境情報
遅いように見えるマシンではJVMの方がメソッドのインラインを行う傾向があります.単純なプライベートメソッド呼び出しのバージョン虚メソッドのバージョンだけでなく、同じです.なぜかというと、JVMはAdderにサブクラスが1つしかないことを発見しました.つまり、抽象メソッドごとに1つのバージョンしかありません.実行時に別のサブクラスをロードした場合、パフォーマンスが直線的に低下します.これ以上インラインできないためです.これにかかわらず、テストから見ると、これらのメソッドの呼び出しはオーバーヘッドが低いわけではありません.オーバーヘッドメソッドの呼び出しはまったくありません.また、可読性のために追加されたドキュメントは、ソースコードとコンパイルされたバイトコードにのみ存在します.実行時には、完全にインラインが消去されます.
私は2番目の結果についてもよく理解していません.性能が高いように見える機器Bは、単一のメソッド呼び出しを実行するときは、早く他の2つを実行すると遅くなります.インラインを遅延させる傾向があるかもしれませんが、差はそれほど大きくありません.スタックトラッキング情報生成の最適化のように、コードのパフォーマンスを最適化するために手動でインラインする方法が膨大で複雑であればあるほど、あなたは本当に間違っています.
オリジナルの文章の転載は出典の原文のリンクを明記してください
詳細はdeepinmindに移動してください