web-dev-qa-db-ja.com

ループ前またはループ内の変数の宣言の違いは?

一般的に、ループ内で繰り返し変数を宣言するのではなく、ループの前にスローアウェイ変数を宣言すると、(パフォーマンス)の違いが生じるのではないかといつも思っていました。 Javaの(まったく無意味)の例:

a)ループ前の宣言:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b)ループ内の宣言(繰り返し):

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

どちらが良いですか、aまたはb

繰り返される変数宣言(例b)は理論上より多くのオーバーヘッドを作成すると思われますが、コンパイラは十分にスマートなので、関係ありません。例bには、よりコンパクトで、変数のスコープが使用される場所に制限されるという利点があります。それでも、例aに従ってコーディングする傾向があります。

Edit:特にJavaの場合に興味があります。

304
Rabarberski

どちらが良いですか、aまたはb

パフォーマンスの観点からは、測定する必要があります。 (そして、私の意見では、差を測定できる場合、コンパイラーはあまり良くありません)。

保守の観点からは、bの方が優れています。可能な限り狭い範囲で、同じ場所で変数を宣言して初期化します。宣言と初期化の間に大きな穴を開けたり、必要のない名前空間を汚したりしないでください。

245

さて、AとBの例をそれぞれ20回実行し、1億回ループしました(JVM-1.5.0)

A:平均実行時間:.074秒

B:平均実行時間:.067秒

驚いたことに、Bは少し速かった。これを正確に測定できるかどうかは、コンピューターと同じくらい速いのです。私もそれをA方法でコーディングしますが、私はそれは本当に重要ではないと言うでしょう。

208
Mark

それは言語と正確な使用法に依存します。たとえば、C#1では違いはありません。 C#2では、ローカル変数が匿名メソッド(またはC#3のラムダ式)によってキャプチャされた場合、非常に大きな違いが生じる可能性があります。

例:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

出力:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

違いは、すべてのアクションが同じouter変数をキャプチャしますが、それぞれに独自の個別のinner変数があることです。

66
Jon Skeet

以下は、私が.NETで作成およびコンパイルしたものです。

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

これは、 。NETリフレクター から得られるものです- CIL がコードに戻されます。

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

したがって、コンパイル後は両方ともまったく同じに見えます。管理言語では、コードはCL /バイトコードに変換され、実行時に機械語に変換されます。そのため、機械語では、スタック上にdoubleが作成されない場合があります。 WriteLine関数の一時変数であることをコードが反映しているため、単なるレジスタである場合があります。ループ専用の最適化ルールが設定されています。そのため、特にマネージド言語では、平均的な人は心配するべきではありません。たとえば、string a; a+=anotherstring[i]StringBuilderを使用して多数の文字列を連結する必要がある場合など、管理コードを最適化できる場合があります。両方のパフォーマンスには非常に大きな違いがあります。コンパイラーがコードを最適化できないようなケースが多くあります。これは、より大きなスコープで何が意図されているかを把握できないためです。ただし、基本的なことはほとんど最適化できます。

35
particle

これはVB.NETの落とし穴です。この例では、Visual Basicの結果は変数を再初期化しません。

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

これにより、最初に0が出力されます(Visual Basic変数は宣言時にデフォルト値になります!)が、その後は毎回iが出力されます。

ただし、= 0を追加すると、期待どおりの結果が得られます。

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...
24
Michael Haren

私は簡単なテストを行いました:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

for (int i = 0; i < 10; i++) {
    int b = i;
}

これらのコードをgcc-5.2.0でコンパイルしました。そして、私はこれらの2つのコードのメイン()を分解しました、そしてそれが結果です:

1º:

   0x00000000004004b6 <+0>:     Push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

   0x00000000004004b6 <+0>: Push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

これはまったく同じ結果です。 2つのコードが同じものを生成するという証拠ではありませんか?

15
UserX

私は常に(コンパイラに依存するのではなく)Aを使用し、次のように書き換えることもあります。

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

これでもintermediateResultがループのスコープに制限されますが、各反復中に再宣言されません。

11
Triptych

言語に依存します-IIRC C#はこれを最適化するため、違いはありませんが、JavaScript(たとえば)は毎回Shebang全体を割り当てます。

11
annakata

私の意見では、bはより良い構造です。 aでは、ループが終了した後、intermediateResultの最後の値が保持されます。

編集:これは値の型とそれほど大きな違いはありませんが、参照型はやや重要です。個人的には、クリーンアップのためにできるだけ早く変数を逆参照するのが好きです。bはそれをあなたのために行います。

6
Powerlord

まあ、あなたは常にそのためのスコープを作ることができます:

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

この方法では、変数を1回宣言するだけで、ループを抜けると消滅します。

5
Marcelo Faísca

ラムダなどで変数を使用している場合、C#には違いがあります。しかし、一般に、コンパイラーは、変数がループ内でのみ使用されると仮定して、基本的に同じことを行います。

基本的に同じであることに注意してください。バージョンbを使用すると、ループの後で変数が使用されないこと、および使用できないことが読者に明らかになります。さらに、バージョンbははるかに簡単にリファクタリングされます。バージョンaでループ本体を独自のメソッドに抽出することはより困難です。さらに、バージョンbは、そのようなリファクタリングに副作用がないことを保証します。

それゆえ、バージョンaは私に終わりを告げません。何の利点もありませんし、コードについて推論するのがずっと難しくなるからです...

5
Mark Sowul

同僚は最初のフォームを好み、それが最適化であることを伝え、宣言を再利用することを好みます。

私は2番目のものを好む(そして私の同僚を説得しようとする!-))、それを読んで:

  • 変数の範囲を必要な場所に限定しますが、これは良いことです。
  • Javaは、パフォーマンスに大きな違いをもたらさないように十分に最適化します。 IIRC、おそらく2番目の形式はさらに高速です。

とにかく、コンパイラやJVMの品質に依存する時期尚早な最適化のカテゴリに分類されます。

5
PhiLho

いくつかのコンパイラが両方を同じコードになるように最適化できると思いますが、すべてではありません。したがって、前者の方が良いと思います。後者の唯一の理由は、宣言された変数がループ内でonlyを使用することを保証したい場合です。

5
Stew S

原則として、可能な限り最も内側のスコープで変数を宣言します。したがって、ループの外でIntermediateResultを使用していない場合は、Bを使用します。

5
Chris

ループ内で変数を宣言すると、メモリが無駄になるといつも思っていました。このようなものがある場合:

for(;;) {
  Object o = new Object();
}

その場合、各反復でオブジェクトを作成する必要があるだけでなく、各オブジェクトに新しい参照を割り当てる必要があります。ガベージコレクターが遅い場合、クリーンアップする必要のあるぶら下がり参照がたくさんあるようです。

ただし、これがある場合:

Object o;
for(;;) {
  o = new Object();
}

次に、単一の参照を作成し、そのたびに新しいオブジェクトを割り当てます。確かに、スコープから外れるまで少し時間がかかるかもしれませんが、処理するぶら下がり参照は1つしかありません。

4
R. Carr

私の練習は次のとおりです。

  • 変数のタイプが単純な場合(int、double、...)私はバリアントb(inside)を好みます。
    理由:変数のスコープを縮小します。

  • 変数の型が単純でない場合(何らかのclassまたはstruct私はバリアントa(外部)を好みます。
    理由:ctor-dtor呼び出しの回数を減らす。

3
fat

それはコンパイラに依存しており、一般的な答えを出すのは難しいと思います。

3
SquidScareMe

パフォーマンスの観点からは、外部の方が(はるかに)優れています。

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

両方の機能をそれぞれ10億回実行しました。 outside()は65ミリ秒かかりました。 inside()には1.5秒かかりました。

1
Alex

興味深い質問です。私の経験から、この問題をコードについて議論する際に考慮すべき究極の質問があります:

変数がグローバルである必要がある理由はありますか?

ローカルで何度も変数を宣言するのではなく、グローバルに1回だけ変数を宣言するのは理にかなっています。コードを整理するのに適していて、必要なコード行が少ないためです。ただし、1つのメソッド内でローカルに宣言するだけでよい場合は、そのメソッドで初期化して、変数がそのメソッドにのみ関連していることを明確にします。後者のオプションを選択した場合、初期化されたメソッドの外部でこの変数を呼び出さないように注意してください。コードは何を話しているかわからず、エラーを報告します。

また、補足として、目的がほぼ同じであっても、異なるメソッド間でローカル変数名を重複させないでください。混乱するだけです。

0
Joshua Siktar

Goで同じことを試し、go 1.9.4でgo tool compile -Sを使用してコンパイラ出力を比較しました

アセンブラー出力によるゼロ差。

0
SteveOC 64

私はこれと同じ質問を長い間持っていました。そこで、さらに単純なコードをテストしました。

結論:そのような場合にはいいえパフォーマンスの違い。

ループ外の場合

int intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i+2;
    System.out.println(intermediateResult);
}

ループ内の場合

for(int i=0; i < 1000; i++){
    int intermediateResult = i+2;
    System.out.println(intermediateResult);
}

IntelliJのデコンパイラでコンパイルされたファイルを確認しましたが、どちらの場合もsameTest.class

for(int i = 0; i < 1000; ++i) {
    int intermediateResult = i + 2;
    System.out.println(intermediateResult);
}

また、この answer で指定された方法を使用して、両方のケースのコードを逆アセンブルしました。答えに関連する部分のみを表示します

ループ外の場合

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field Java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method Java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

ループ内の場合

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field Java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method Java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

細心の注意を払うと、Slot内のiおよびintermediateResultに割り当てられたLocalVariableTableのみが、出現順序の積としてスワップされます。スロットの同じ違いは、他のコード行にも反映されます。

  • 追加の操作は実行されていません
  • intermediateResultはどちらの場合もローカル変数なので、アクセス時間に違いはありません。

ボーナス

コンパイラは大量の最適化を行い、この場合に何が起こるかを見てみましょう。

ゼロワークケース

for(int i=0; i < 1000; i++){
    int intermediateResult = i;
    System.out.println(intermediateResult);
}

ゼロワークの逆コンパイル

for(int i = 0; i < 1000; ++i) {
    System.out.println(i);
}
0
twitu

これは良い形です

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1)このようにして、サイクルごとにではなく、両方の変数を一度宣言します。 2)他のすべてのオプションの割り当て。 3)したがって、ベストプラクティスのルールは、反復の外の宣言です。

0
luka

A)B)よりも安全な賭けです.........「int」または「float」ではなく、ループで構造を初期化する場合を想像してください。

のような

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

メモリリークの問題に直面することは確かです!したがって、「A」の方がより安全な賭けであり、「B」は、特にソースライブラリを操作するメモリ蓄積に対して脆弱であると考えています。

0

誰かが興味があるなら、Node 4.0.0でJSをテストしました。ループ外で宣言すると、1回の試行あたり1億回のループ反復を行う1000回の試行で、平均で〜.5ミリ秒のパフォーマンスの改善がもたらされました。だから私は先に行くと言って、最も読みやすい/保守可能な方法でそれを書くつもりです、それはB、imoです。私は自分のコードをいじくりますが、今はパフォーマンスのNodeモジュールを使用しました。コードは次のとおりです。

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)
0
user137717