ループの内部または外部で変数を宣言する


236

以下はなぜうまくいくのですか?

String str;
while (condition) {
    str = calculateStr();
    .....
}

しかし、これは危険/不正であると言われています:

while (condition) {
    String str = calculateStr();
    .....
}

ループの外で変数を宣言する必要がありますか?

回答:


289

ローカル変数のスコープは常に可能な限り最小にする必要があります。

あなたの例では、私の推測がstrされていないの外使用whileの内側にそれを宣言しているため、それ以外の場合は、あなたが質問をされない、ループをwhileループするオプションではありません、それはコンパイルされないであろうから、。

したがって、以降はstrされないための最小の可能な範囲は、ループ外で使用strされ whileループ。

したがって、答えは、whileループ内で絶対的に宣言する必要があるということを強調strています。ifs、nos、notはありません。

このルールに違反する唯一のケースは、何らかの理由ですべてのクロックサイクルをコードから絞り出す必要がある場合に非常に重要です。その場合、外部スコープでインスタンス化し、それを再利用することを検討します。内部スコープのすべての反復でそれを再インスタンス化します。ただし、これはJavaの文字列が不変であるため、この例には当てはまりません。strの新しいインスタンスは常にループの最初に作成され、ループの最後で破棄する必要があるため、そこで最適化する可能性はありません。

編集:(私のコメントを以下の回答に挿入します)

いずれにせよ、適切な方法は、すべてのコードを適切に記述し、製品のパフォーマンス要件を確立し、この要件に対して最終製品を測定し、それを満たさない場合は、最適化することです。そして、通常、最終的に発生するのは、コードベース全体を調べて調整し、ハックする代わりに、プログラムがパフォーマンス要件を満たすようにするためのいくつかの場所で、いくつかの優れた正式なアルゴリズム最適化を提供する方法を見つけることです。あちこちでクロックサイクルを圧迫するために。


2
最後の段落のクエリ:それが不変でない別の文字列である場合、それは影響しますか?
Harry Joy、

1
@HarryJoyはい、もちろん、変更可能なStringBuilderを例にとります。StringBuilderを使用してループの各反復で新しい文字列を作成する場合、ループの外側でStringBuilderを割り当てることにより、物事を最適化できます。しかし、それでも、これは賢明な方法ではありません。非常に正当な理由なしにそれを行う場合、それは時期尚早の最適化です。
Mike Nakis、2012年

7
@HarryJoy物事を行う正しい方法は、すべてのコードを適切に記述し、製品のパフォーマンス要件を確立し、この要件に対して最終製品を測定し、それがそれを満たさない場合は、物事を最適化することです。そして、あなたは何を知っていますか?通常、コードベース全体を調べて微調整したりハッキングしたりして、クロックサイクルをあちこちに詰め込む代わりに、いくつかの場所でいくつかの優れた正式なアルゴリズム最適化を提供できます。
Mike Nakis、2012年

2
@MikeNakis非常に狭い範囲で考えていること。
Siten 2012年

5
ご覧のとおり、最新のマルチギガヘルツ、マルチコア、パイプライン、マルチレベルのメモリキャッシュCPUにより、クロックサイクルを気にすることなく、次のベストプラクティスに集中できます。さらに、最適化は、それが必要であると判断された場合にのみ推奨され、必要に応じて、高度にローカライズされた2、3の調整により通常は望ましいパフォーマンスが達成されるため、すべてのコードを散らかす必要はありません。パフォーマンスの名の下に小さなハックで。
Mike Nakis、2012年

293

これら2つの(類似した)例のバイトコードを比較しました。

1.例を見てみましょう。

package inside;

public class Test {
    public static void main(String[] args) {
        while(true){
            String str = String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

後はjavac Test.javajavap -c Test次のようになります。

public class inside.Test extends java.lang.Object{
public inside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

2.例を見てみましょう。

package outside;

public class Test {
    public static void main(String[] args) {
        String str;
        while(true){
            str =  String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

後はjavac Test.javajavap -c Test次のようになります。

public class outside.Test extends java.lang.Object{
public outside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

観察結果は、これら2つの例の間に違いないことを示しています。それはJVM仕様の結果です...

ただし、コーディングのベストプラクティスの名の下で、可能な限り小さいスコープで変数を宣言することをお勧めします(この例では、変数が使用される唯一の場所であるため、ループ内にあります)。


3
これは「コンパイラの最適化」ではなく、JVM Soecificationの結果です。メソッドに必要なスタックスロットはすべて、メソッドへのエントリ時に割り当てられます。これがバイトコードの指定方法です。
ローンの侯爵

2
@Arhimedループ(または単に '{}'ブロック)内に配置するもう1つの理由があります。コンパイラーは、スタックフレームに割り当てられたメモリを別のスコープ内の変数に再利用します。 。
セルジュ

1
データオブジェクトのリストを介してループする場合、大量のデータに対して何か違いがありますか?おそらく40千。
Mithun Khatri

7
あなたのfinal愛好家にとって:パッケージの場合とstr同じようfinalに宣言して違いはありません=)inside
skia.heliou

27

最小スコープでオブジェクトを宣言すると、読みやすさが向上します。

今日のコンパイラではパフォーマンスは問題になりません。(このシナリオでは)
保守の観点からは、2番目のオプションの方が優れています。
可能な限り狭い範囲で、同じ場所で変数を宣言して初期化します。

以下のようドナルド・アーヴィン・クヌースは言いました:

「私たちは小さな効率については忘れる必要があります。たとえば、約97%の時間を言います。時期尚早の最適化がすべての悪の根源です」

つまり、プログラマーがパフォーマンスを考慮してコードの設計に影響を与える状況。これにより、コードが最適化によって複雑になり、プログラマーが最適化によって気が散らされるため、デザインがきれいになっていない場合や、コードが正しくない場合があります。


1
「2番目のオプションの方がパフォーマンスが少し速い」 =>測定しましたか?答えの1つによると、バイトコードは同じなので、パフォーマンスがどのように異なるかはわかりません。
assylias 2012

申し訳ありませんが、それは実際にJavaプログラムのパフォーマンスをテストする正しい方法ではありません(そして、とにかく、無限ループのパフォーマンスをテストするにはどうすればよいですか?)
assylias

他の点にも同意します。パフォーマンスに違いはないと私は信じています。
アッシリア2012

11

str外部ループも使用したい場合。外で宣言してください。それ以外の場合は、2番目のバージョンで問題ありません。


11

更新された回答にスキップしてください...

パフォーマンスを重視する場合は、System.outを取り出し、ループを1バイトに制限します。double(テスト1/2)とString(3/4)を使用した経過時間(ミリ秒)は、Windows 7 Professional 64ビットとJDK-1.7.0_21で以下に示されています。バイトコード(test1とtest2についても以下に示す)は同じではありません。変更可能で比較的複雑なオブジェクトをテストするのが面倒でした。

ダブル

Test1の所要時間:2710ミリ秒

Test2の所要時間:2790ミリ秒

文字列(テストではdoubleを文字列に置き換えるだけです)

Test3の所要時間:1200ミリ秒

Test4の所要時間:3000ミリ秒

バイトコードのコンパイルと取得

javac.exe LocalTest1.java

javap.exe -c LocalTest1 > LocalTest1.bc


public class LocalTest1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        double test;
        for (double i = 0; i < 1000000000; i++) {
            test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }

}

public class LocalTest2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (double i = 0; i < 1000000000; i++) {
            double test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }
}


Compiled from "LocalTest1.java"
public class LocalTest1 {
  public LocalTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore        5
       7: dload         5
       9: ldc2_w        #3                  // double 1.0E9d
      12: dcmpg
      13: ifge          28
      16: dload         5
      18: dstore_3
      19: dload         5
      21: dconst_1
      22: dadd
      23: dstore        5
      25: goto          7
      28: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      31: lstore        5
      33: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: new           #6                  // class java/lang/StringBuilder
      39: dup
      40: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      43: ldc           #8                  // String Test1 Took:
      45: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      48: lload         5
      50: lload_1
      51: lsub
      52: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      55: ldc           #11                 // String  msecs
      57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      60: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      63: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: return
}


Compiled from "LocalTest2.java"
public class LocalTest2 {
  public LocalTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore_3
       6: dload_3
       7: ldc2_w        #3                  // double 1.0E9d
      10: dcmpg
      11: ifge          24
      14: dload_3
      15: dstore        5
      17: dload_3
      18: dconst_1
      19: dadd
      20: dstore_3
      21: goto          6
      24: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      27: lstore_3
      28: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: new           #6                  // class java/lang/StringBuilder
      34: dup
      35: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      38: ldc           #8                  // String Test1 Took:
      40: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      43: lload_3
      44: lload_1
      45: lsub
      46: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      49: ldc           #11                 // String  msecs
      51: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      54: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      57: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: return
}

更新された回答

パフォーマンスをすべてのJVM最適化と比較することは本当に簡単ではありません。ただし、多少は可能です。Google Caliperでのより良いテストと詳細な結果

  1. ブログの詳細:ループ内またはループの前に変数を宣言する必要がありますか?
  2. GitHubリポジトリ:https : //github.com/gunduru/jvdt
  3. ダブルケースと100Mループのテスト結果(およびすべてのJVM詳細):https : //microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

DeclaredBefore 1,759.209 DeclaredInside 2,242.308

  • 宣言前1,759.209 ns
  • 宣言された内部2,242.308 ns

二重宣言の部分テストコード

これは上記のコードと同じではありません。ダミーループをコーディングするだけの場合、JVMはそれをスキップするので、少なくとも何かを割り当てて返す必要があります。これはCaliperのドキュメントでも推奨されています。

@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Declaration and assignment */
        double test = i;

        /* Dummy assignment to fake JVM */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {

    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Actual test variable */
    double test = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Assignment */
        test = i;

        /* Not actually needed here, but we need consistent performance results */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

要約:declaredBeforeは、パフォーマンスが非常に小さいことを示しています。これは、スコープの最小原理に反しています。JVMが実際にこれを行うはずです


テスト方法が無効であり、結果の説明を提供していません。
ローン侯爵14

1
@EJPこれは、このテーマに関心がある人にとってはかなり明確なはずです。方法論は、より有用な情報を提供するためにPrimosKの回答からとられています。正直に言うと、この答えを改善する方法がわかりません。おそらく、[編集]をクリックして、適切な方法を教えてください。
OnurGünduru2014

2
1)Java Bytecodeは実行時に最適化(並べ替え、折りたたみなど)されるため、.classファイルに何が書き込まれるかを気にしないでください。2)パフォーマンスが2.8秒向上する1.000.000.000の実行があるため、安全で適切なプログラミングスタイルに対して、実行あたり約2.8nsです。私にとって明確な勝者。3)ウォームアップに関する情報を提供していないため、タイミングがまったく役に立たない。
2015年

二重および100Mループに対してのみ、キャリパーを使用したハードコード化された優れたテスト/マイクロベンチマーク。他のケースが必要な場合は、オンラインで結果を編集してください。
OnurGünduru2015年

おかげで、これはポイント1)と3)を排除します。しかし、時間がサイクルあたり最大5nsになったとしても、これはまだ無視すべき時間です。理論上は小さな最適化の可能性がありますが、実際には、サイクルごとに行う作業は通常、はるかに高価です。したがって、数分または数時間の実行で最大で数秒になる可能性があります。この種の低レベルの最適化に時間を費やす前にチェックする可能性のある、より高い利用可能性のある他のオプション(フォーク/ジョイン、並列ストリームなど)があります。
2015年

7

この問題の1つの解決策は、whileループをカプセル化する変数スコープを提供することです。

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}

外側のスコープが終了すると、自動的に逆参照されます。


6

内部では、変数のスコープが小さいほど、変数の可視性が高くなります。


5

strafterループ(スコープ関連)を使用する必要がない場合は、2番目の条件、つまり

  while(condition){
        String str = calculateStr();
        .....
    }

conditiontrueの場合にのみスタック上にオブジェクトを定義すると、つまり、必要に応じて使用します


2
最初のバリアントでも、条件が偽の場合、オブジェクトは作成されないことに注意してください。
Philipp Wendler、2012年

@フィリップ:はいあなたは正しいです。私の悪い。今のまま考えていましたがどう思いますか?
Cratylus 2012年

1
「スタック上のオブジェクトを定義する」というのは、Javaの世界ではやや奇妙な用語です。また、スタックに変数を割り当てることは、通常、実行時に何もしないので、なぜわざわざ?プログラマーを助けるスコーピングは本当の問題です。
Philipp Wendler、2012年

3

あなたの質問に答えるのに最適なリソースは次の投稿だと思います:

ループの前またはループで変数を宣言することの違いは?

私の理解によると、これは言語に依存するでしょう。IIRC Javaはこれを最適化するため、違いはありませんが、JavaScript(たとえば)はループ内で毎回メモリ全体の割り当てを行います。特にJavaでは、プロファイリングを実行すると、2番目がより速く実行されると思います。


3

多くの人が指摘しているように、

String str;
while(condition){
    str = calculateStr();
    .....
}

これよりは良くありません

while(condition){
    String str = calculateStr();
    .....
}

したがって、再利用しない場合は、スコープ外の変数を宣言しないでください...


1
おそらくこの方法を除いて:リンク
Dainius Kreivys

2

wileループの外側でString strを宣言すると、whileループの内側と外側で参照できます。whileループ内でString strを宣言すると、whileループ内でのみ参照できます。


1

変数は、使用される場所にできるだけ近い場所で宣言する必要があります。

RAII (Resource Acquisition Is Initialization)が簡単になります。

それは変数のスコープをタイトに保ちます。これにより、オプティマイザの動作が向上します。



1

str変数が利用可能であるとさえコードの下に実行しながら、後にメモリ内の一部の空間を予約します。

 String str;
    while(condition){
        str = calculateStr();
        .....
    }

str変数が使用できなくなり、またメモリのために割り当てられたリリースされるstrコードの下に変数。

while(condition){
    String str = calculateStr();
    .....
}

2番目の方法を確実に実行すると、システムメモリが減少し、パフォーマンスが向上します。


0

ループ内で宣言すると、それぞれの変数のスコープが制限されます。それはすべて、変数のスコープに関するプロジェクトの要件に依存します。


0

確かに、上記の質問はプログラミングの問題です。コードをどのようにプログラムしますか?どこに「STR」にアクセスする必要がありますか?ローカルでグローバル変数として使用される変数を宣言する必要はありません。私は信じているプログラミングの基礎。


-1

これら2つの例の結果は同じです。ただし、最初の例ではstr、whileループの外で変数を使用できます。2番目は違います。


-1

この質問のほとんどすべての人への警告:Java 7を使用しているコンピューターでは、ループ内で200倍の速度になりやすいサンプルコードを次に示します(メモリの消費量も少し異なります)。しかし、それは割り当てに関するものであり、スコープだけではありません。

public class Test
{
    private final static int STUFF_SIZE = 512;
    private final static long LOOP = 10000000l;

    private static class Foo
    {
        private long[] bigStuff = new long[STUFF_SIZE];

        public Foo(long value)
        {
            setValue(value);
        }

        public void setValue(long value)
        {
            // Putting value in a random place.
            bigStuff[(int) (value % STUFF_SIZE)] = value;
        }

        public long getValue()
        {
            // Retrieving whatever value.
            return bigStuff[STUFF_SIZE / 2];
        }
    }

    public static long test1()
    {
        long total = 0;

        for (long i = 0; i < LOOP; i++)
        {
            Foo foo = new Foo(i);
            total += foo.getValue();
        }

        return total;
    }

    public static long test2()
    {
        long total = 0;

        Foo foo = new Foo(0);
        for (long i = 0; i < LOOP; i++)
        {
            foo.setValue(i);
            total += foo.getValue();
        }

        return total;
    }

    public static void main(String[] args)
    {
        long start;

        start = System.currentTimeMillis();
        test1();
        System.out.println(System.currentTimeMillis() - start);

        start = System.currentTimeMillis();
        test2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

結論:ローカル変数のサイズによっては、それほど大きな変数でなくても、その差は非常に大きくなる可能性があります。

ループの外側または内側が問題になることもあります。


1
確かに、2番目の方が高速ですが、別のことを行っています。test1は大きな配列を持つ多くのFoo-Objectsを作成していますが、test2はそうではありません。test2は同じFooオブジェクトを繰り返し再利用しています。これはマルチスレッド環境では危険な場合があります。
2015年

マルチスレッド環境では危険??? 理由を説明してください。ローカル変数について話している。メソッドの呼び出しごとに作成されます。
rt15

Foo-Objectを、データを非同期で処理する操作に渡している場合、データを変更している間、操作はFooインスタンスで機能している可能性があります。副作用を持たせるためにマルチスレッド化する必要さえありません。したがって、インスタンスをまだ誰が使用しているのかわからない場合、インスタンスの再利用は非常に危険
です

Ps:setValueメソッドはbigStuff[(int) (value % STUFF_SIZE)] = value;(2147483649Lの値を試してください)
'08年

副作用について話す:あなたはあなたの方法の結果を比較しましたか?
2015年

-1

オブジェクトのサイズも重要だと思います。私のプロジェクトの1つで、アプリケーションがメモリ不足の例外をスローするようにする大きな2次元配列を宣言して初期化しました。代わりに宣言をループの外に移動し、すべての反復の開始時に配列をクリアしました。


-2

あなたはリスク持っているNullPointerExceptionあなたの場合はcalculateStr()メソッドの戻りはヌルをし、その後、あなたはstrの上のメソッドを呼び出すようにしてください。

より一般的には、null値の変数を持つことは避けてください。ちなみに、それはクラス属性に対してより強力です。


2
これは質問に関連する方法ではありません。(将来の関数呼び出しで)NullPointerExceptionが発生する確率は、変数の宣言方法には依存しません。
デザートアイス

1
「それを行うための最良の方法は何か」という質問なので、私はそうは思いません。私見私はより安全なコードを望みます。
レミDoolaeghe

1
NullPointerException.このコードを実行しようreturn str;とすると、コンパイルエラーが発生するリスクはありません。
ローンの侯爵2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.