web-dev-qa-db-ja.com

2つの別々のループが1つよりも速いのはなぜですか?

Javaが連続するforループに対してどのような最適化を行うのかを理解したい。より正確には、ループ融合が実行されているかどうかを確認しようとしています。理論的には、この最適化が行われていないと予想していました自動的に、融合バージョンが2つのループを持つバージョンよりも高速であることを確認することを期待していました。

ただし、ベンチマークを実行した後、結果は、2つの別々の(および連続した)ループが、すべての作業を実行する1つの単一のループよりも高速であることを示しています。

ベンチマークの作成にJMHを使用してみましたが、同じ結果が得られました。

javapコマンドを使用しましたが、2つのループを持つソースファイルに対して生成されたバイトコードは、実際には実行中の2つのループに対応していることが示されています(ループの展開やその他の最適化は実行されていません)。

_BenchmarkMultipleLoops.Java_について測定されているコード:

_private void work() {
        List<Capsule> intermediate = new ArrayList<>();
        List<String> res = new ArrayList<>();
        int totalLength = 0;

        for (Capsule c : caps) {
            if(c.getNumber() > 100000000){
                intermediate.add(c);
            }
        }

        for (Capsule c : intermediate) {
            String s = "new_Word" + c.getNumber();
            res.add(s);
        }

        //Loop to assure the end result (res) is used for something
        for(String s : res){
            totalLength += s.length();
        }

        System.out.println(totalLength);
    }
_

_BenchmarkSingleLoop.Java_について測定されているコード:

_private void work(){
        List<String> res = new ArrayList<>();
        int totalLength = 0;

        for (Capsule c : caps) {
            if(c.getNumber() > 100000000){
                String s = "new_Word" + c.getNumber();
                res.add(s);
            }
        }

        //Loop to assure the end result (res) is used for something
        for(String s : res){
            totalLength += s.length();
        }

        System.out.println(totalLength);
    }
_

そして、ここに_Capsule.Java_のコードがあります:

_public class Capsule {
    private int number;
    private String Word;

    public Capsule(int number, String Word) {
        this.number = number;
        this.Word = Word;
    }

    public int getNumber() {
        return number;
    }

    @Override
    public String toString() {
        return "{" + number +
                ", " + Word + '}';
    }
}
_

capsは_ArrayList<Capsule>_で、最初は2,000万個の要素が次のように入力されています。

_private void populate() {
        Random r = new Random(3);

        for(int n = 0; n < POPSIZE; n++){
            int randomN = r.nextInt();
            Capsule c = new Capsule(randomN, "Word" + randomN);
            caps.add(c);
        }
    }
_

測定前に、ウォームアップフェーズが実行されます。

各ベンチマークを10回実行しました。つまり、work()メソッドをベンチマークごとに10回実行し、完了するまでの平均時間を以下に示します(秒単位)。各反復の後、GCはいくつかのスリープとともに実行されました。

  • MultipleLoops:4.9661秒
  • シングルループ:7.2725秒

Intel i7-7500U(Kaby Lake)で実行されているOpenJDK1.8.0_144。

2つの異なるデータ構造をトラバースする必要があるにもかかわらず、MultipleLoopsバージョンがSingleLoopバージョンよりも高速なのはなぜですか?

更新1:

コメントで示唆されているように、文字列の生成中にtotalLengthを計算するように実装を変更し、resリストの作成を回避すると、シングルループバージョンが高速になります。

ただし、その変数は、要素が何も行われなかった場合に要素が破棄されないようにするために、結果リストの作成後に一部の作業が行われるようにするためにのみ導入されました。

言い換えると、意図した結果は、最終的なリストを生成することです。しかし、この提案は、何が起こっているのかをよりよく理解するのに役立ちます。

結果:

  • MultipleLoops:0.9339秒
  • SingleLoop:0.66590005秒

更新2:

JMHベンチマークに使用したコードへのリンクは次のとおりです。 https://Gist.github.com/FranciscoRibeiro/2d3928761f76e4f7cecfcfcdf7fc96d5

結果:

  • MultipleLoops:7.397秒
  • シングルループ:8.092秒
23

私はこの「現象」を調査したところ、答えのようなものが得られたようです。
JMH OptionsBuilder.jvmArgs("-verbose:gc")を追加しましょう。 1回の反復の結果:

シングルループ:[フルGC(人間工学)[PSYoungGen:2097664K-> 0K(2446848K)] [ParOldGen:3899819K-> 4574771K(5592576K)] 5997483K-> 4574771K(8039424K)、[メタスペース:6208K-> 6208K(1056768K)] 、5.0438301秒] [時間:ユーザー= 37.92sys = 0.10、実数= 5.05秒] 4.954秒/操作

複数のループ:[フルGC(人間工学)[PSYoungGen:2097664K-> 0K(2446848K)] [ParOldGen:3899819K-> 4490913K(5592576K)] 5997483K-> 4490913K(8039424K)、[メタスペース:6208K-> 6208K(1056768K)] 、3.7991573秒] [時間:ユーザー= 26.84 sys = 0.08、実数= 3.80秒] 4.187秒/操作

JVMはGCに膨大なCPU時間を費やしました。 2回のテスト実行ごとに1回、JVMはフルGCを作成する必要があります(600MbをOldGenに移動し、前のサイクルから1.5Gbのゴミを収集します)。両方のガベージコレクターは同じ作業を行いましたが、複数のループのテストケースに費やすアプリケーション時間は約25%短縮されました。 POPSIZEを10_000_000に減らすか、bh.consume()Thread.sleep(3000)の前に追加するか、JVM引数に_-XX:+UseG1GC_を追加すると、複数ループのブースト効果がなくなります。 .addProfiler(GCProfiler.class)でもう一度実行します。主な違い:

複数のループ:gc.churn.PS_Eden_Space374.417±23MB /秒

シングルループ:gc.churn.PS_Eden_Space 336.037MB /秒±19MB /秒

古き良きコンペアアンドスワップGCアルゴリズムには複数のテスト実行のCPUボトルネックがあり、以前の実行からのガベージの収集に余分な「無意味な」サイクルを使用するため、このような特定の状況ではスピードが上がると思います。十分なRAMがあれば、@Threads(2)で再現するのはさらに簡単です。 Single_Loopテストのプロファイルを作成しようとすると、次のようになります。

profiling

2
Anton Kot

内部で何が起こっているかを理解するために、Java_HOME\binにあるjvisualvmで実行中のアプリを分析するJMX動作を追加できます。メモリ内に20Mサイズのカプセルリストがあるため、メモリが不足し、visualvmが応答しない状態になりました。テストする条件で、カプセルリストのサイズを200kに、100Mを1Mに減らしました。 Visualvmでの動作を観察した後、複数のループの前に単一のループの実行が完了しました。これは正しいアプローチではないかもしれませんが、試してみることができます。

LoopBean.Java

import Java.util.List;
public interface LoopMBean {
    void multipleLoops();
    void singleLoop();
    void printResourcesStats();
}

Loop.Java

import Java.util.ArrayList;
import Java.util.List;
import Java.util.Random;

public class Loop implements LoopMBean {

    private final List<Capsule> capsules = new ArrayList<>();

    {
        Random r = new Random(3);
        for (int n = 0; n < 20000000; n++) {
            int randomN = r.nextInt();
            capsules.add(new Capsule(randomN, "Word" + randomN));
        }
    }

    @Override
    public void multipleLoops() {

        System.out.println("----------------------Before multiple loops execution---------------------------");
        printResourcesStats();

        final List<Capsule> intermediate = new ArrayList<>();
        final List<String> res = new ArrayList<>();
        int totalLength = 0;

        final long start = System.currentTimeMillis();

        for (Capsule c : capsules)
            if (c.getNumber() > 100000000) {
                intermediate.add(c);
            }

        for (Capsule c : intermediate) {
            String s = "new_Word" + c.getNumber();
            res.add(s);
        }

        for (String s : res)
            totalLength += s.length();

        System.out.println("multiple loops=" + totalLength + " | time taken=" + (System.currentTimeMillis() - start) + " milliseconds");

        System.out.println("----------------------After multiple loops execution---------------------------");
        printResourcesStats();

        res.clear();
    }

    @Override
    public void singleLoop() {

        System.out.println("----------------------Before single loop execution---------------------------");
        printResourcesStats();

        final List<String> res = new ArrayList<>();
        int totalLength = 0;

        final long start = System.currentTimeMillis();

        for (Capsule c : capsules)
            if (c.getNumber() > 100000000) {
                String s = "new_Word" + c.getNumber();
                res.add(s);
            }

        for (String s : res)
            totalLength += s.length();

        System.out.println("Single loop=" + totalLength + " | time taken=" + (System.currentTimeMillis() - start) + " milliseconds");
        System.out.println("----------------------After single loop execution---------------------------");
        printResourcesStats();

        res.clear();
    }

    @Override
    public void printResourcesStats() {
        System.out.println("Max Memory= " + Runtime.getRuntime().maxMemory());
        System.out.println("Available Processors= " + Runtime.getRuntime().availableProcessors());
        System.out.println("Total Memory= " + Runtime.getRuntime().totalMemory());
        System.out.println("Free Memory= " + Runtime.getRuntime().freeMemory());
    }
}

LoopClient.Java

import javax.management.MBeanServer;
import javax.management.ObjectName;
import Java.lang.management.ManagementFactory;

public class LoopClient {

    void init() {

        final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        try {
            mBeanServer.registerMBean(new Loop(), new ObjectName("LOOP:name=LoopBean"));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        final LoopClient client = new LoopClient();
        client.init();
        System.out.println("Loop client is running...");
        waitForEnterPressed();
    }

    private static void waitForEnterPressed() {
        try {
            System.out.println("Press  to continue...");
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

次のコマンドで実行します。

Java -Dcom.Sun.management.jmxremote -Dcom.Sun.management.jmxremote.port=9999 -Dcom.Sun.management.jmxremote.authenticate=false -Dcom.Sun.management.jmxremote.ssl=false LoopClient

OutOfMemoryErrorを回避するために、メモリをすばやく増やすための-Xmx3072M追加オプションを追加できます。

1
Aditya