11

Java で入れ子になった for、while、および do-while ループの効率を比較していますが、理解に助けが必要な奇妙な結果に出くわしました。

public class Loops {
    public static void main(String[] args) {
        int L = 100000;    // number of iterations per loop
        // for loop
        double start = System.currentTimeMillis();
        long s1 = 0;
        for (int i=0; i < L; i++) {
            for (int j = 0; j < L; j++) {
                s1 += 1;
            }
        }
        double end = System.currentTimeMillis();
        String result1 = String.format("for loop: %.5f", (end-start) / 1000);
        System.out.println(s1);
        System.out.println(result1);

        // do-while loop
        double start1 = System.currentTimeMillis();
        int i = 0;
        long s2 = 0;
        do {
            i++;
            int j = 0;
            do {
                s2 += 1;
                j++;
            } while (j < L);
        } while (i < L);
        double end1 = System.currentTimeMillis();
        String result2 = String.format("do-while: %.5f", (end1-start1) / 1000);
        System.out.println(s2);
        System.out.println(result2);

        // while loop
        double start2 = System.currentTimeMillis();
        i = 0;
        long s3 = 0;
        while (i < L) {
            i++;
            int j = 0;
            while (j < L) {
                s3 += 1;
                j++;
            }
        }
        double end2 = System.currentTimeMillis();
        String result3 = String.format("while: %.5f", (end2-start2) / 1000);
        System.out.println(s3);
        System.out.println(result3);
    }
}

すべてのループのそれぞれのカウンターの合計は 100 億になります。結果は私を困惑させます:

for ループ: 6.48300

do-while: 0.41200

中: 9.71500

do-while ループがなぜこれほど高速なのか? このパフォーマンス ギャップは、L への変更と並行してスケーリングします。これらのループを個別に実行しましたが、同じように実行されます。

4

1 に答える 1

22

提供されたコードを実行しましたが、これらのパフォーマンスの違いにも驚きました。好奇心に駆られて調査を開始したところ、実際にはこれらのループが同じことをしているように見えますが、それらの間にはいくつかの重要な違いがあることがわかりました。

これらのループを最初に実行した後の結果は次のとおりです。

for loop: 1.43100
do-while: 0.51300
while: 1.54500

しかし、これら 3 つのループを少なくとも 10 回実行すると、これらの各ループのパフォーマンスはほとんど同じでした。

for loop: 0.43200
do-while: 0.46100
while: 0.42900

JIT は時間の経過とともにこれらのループを最適化できますが、これらのループの初期パフォーマンスが異なる原因となる相違点があるはずです。実際には、次の 2 つの違いがあります。

  • do-whileループはforandwhileループよりも少ない比較を行っています

簡単にするために、L = 1 と仮定します。

long s1 = 0;
for (int i=0; i < L; i++) {
    for (int j = 0; j < L; j++) {
        s1 += 1;

外側のループ: 0 < 1
内側のループ: 0 < 1
内側のループ: 1 < 1
外側のループ: 1 < 1

合計4回の比較

int i = 0;
long s2 = 0;
do {
    i++;
    int j = 0;
    do {
        s2 += 1;
        j++;
    } while (j < L);
} while (i < L);

内側のループ: 1 < 1
外側のループ: 1 < 1

計2回の比較

  • 生成された異なるバイトコード

さらなる調査のために、私はあなたのクラスをわずかに変更しましたが、動作に影響はありません。

public class Loops {
    final static int L = 100000; // number of iterations per loop

    public static void main(String[] args) {
        int round = 10;
        while (round-- > 0) {
            forLoop();
            doWhileLoop();
            whileLoop();
        }
    }

    private static long whileLoop() {
        int i = 0;
        long s3 = 0;
        while (i++ < L) {
            int j = 0;
            while (j++ < L) {
                s3 += 1;
            }
        }
        return s3;
    }

    private static long doWhileLoop() {
        int i = 0;
        long s2 = 0;
        do {
            int j = 0;
            do {
                s2 += 1;
            } while (++j < L);
        } while (++i < L);
        return s2;
    }

    private static long forLoop() {
        long s1 = 0;
        for (int i = 0; i < L; i++) {
            for (int j = 0; j < L; j++) {
                s1 += 1;
            }
        }
        return s1;
    }
}

次に、それをコンパイルし、呼び出しjavap -c -s -private -l Loopてバイトコードを取得しました。

まずはdoWhileLoopのバイトコード。

   0:   iconst_0        // push the int value 0 onto the stack
   1:   istore_1        // store int value into variable 1 (i)
   2:   lconst_0        // push the long 0 onto the stack
   3:   lstore_2        // store a long value in a local variable 2 (s2)
   4:   iconst_0        // push the int value 0 onto the stack
   5:   istore  4   // store int value into variable 4 (j)
   7:   lload_2     // load a long value from a local variable 2 (i)
   8:   lconst_1        // push the long 1 onto the stack
   9:   ladd        // add two longs
   10:  lstore_2        // store a long value in a local variable 2 (i)
   11:  iinc    4, 1    // increment local variable 4 (j) by signed byte 1
   14:  iload   4   // load an int value from a local variable 4 (j)
   16:  iload_0     // load an int value from a local variable 0 (L)
   17:  if_icmplt   7   // if value1 is less than value2, branch to instruction at 7
   20:  iinc    1, 1    // increment local variable 1 (i) by signed byte 1
   23:  iload_1     // load an int value from a local variable 1 (i)
   24:  iload_0     // load an int value from a local variable 0 (L)
   25:  if_icmplt   4   // if value1 is less than value2, branch to instruction at 4
   28:  lload_2     // load a long value from a local variable 2 (s2)
   29:  lreturn     // return a long value

whileLooP のバイトコードは次のとおりです。

   0:   iconst_0        // push int value 0 onto the stack
   1:   istore_1        // store int value into variable 1 (i)
   2:   lconst_0        // push the long 0 onto the stack
   3:   lstore_2        // store a long value in a local variable 2 (s3)
   4:   goto        26
   7:   iconst_0        // push the int value 0 onto the stack
   8:   istore  4   // store int value into variable 4 (j)
   10:  goto        17
   13:  lload_2     // load a long value from a local variable 2 (s3)
   14:  lconst_1        // push the long 1 onto the stack
   15:  ladd        // add two longs
   16:  lstore_2        // store a long value in a local variable 2 (s3)
   17:  iload   4   // load an int value from a local variable 4 (j)
   19:  iinc    4, 1    // increment local variable 4 (j) by signed byte 1
   22:  iload_0     // load an int value from a local variable 0 (L)
   23:  if_icmplt   13  // if value1 is less than value2, branch to instruction at 13
   26:  iload_1     // load an int value from a local variable 1 (i)
   27:  iinc    1, 1    // increment local variable 1 by signed byte 1
   30:  iload_0     // load an int value from a local variable 0 (L)
   31:  if_icmplt   7   // if value1 is less than value2, branch to instruction at 7
   34:  lload_2     // load a long value from a local variable 2 (s3)
   35:  lreturn     // return a long value

出力を読みやすくするために、Java バイトコード命令リストに基づいて各命令が何をするかを説明するコメントを追加しました。

よく見ると、これら 2 つのバイトコードには重要な違いがあることがわかります。while ループ (for ループも同様) ではif_icmplt、バイトコードの最後に if ステートメント (命令) が定義されています。つまり、最初のループの終了条件をチェックするには、26 行目への goto を呼び出す必要があり、2 番目のループの 17 行目への goto も同様に呼び出す必要があります。

上記のバイトコードは、Mac OS X で javac 1.6.0_45 を使用して生成されました。

概要

異なる量の比較に加えて、while および for ループ バイトコード内の goto 命令の存在が、これらのループ間のパフォーマンスの違いの原因になっていると思います。

于 2013-05-09T13:59:23.793 に答える