336

一般に、ループ内で繰り返し宣言するのではなく、ループの前にスローアウェイ変数を宣言すると、(パフォーマンス) 違いが生じるのではないかといつも思っていました。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);
}

ab、どっちがいい?

変数宣言を繰り返すと (例b ) 、理論的にはオーバーヘッドが増えると思われますが、コンパイラは問題にならないほど十分にスマートです。例bには、よりコンパクトで、変数のスコープが使用される場所に限定されるという利点があります。それでも、私は例aに従ってコーディングする傾向があります。

編集:私は特に Java のケースに興味があります。

4

26 に答える 26

271

a とbではどちらが良いですか?

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

メンテナンスの観点からは、bの方が優れています。可能な限り狭いスコープで、同じ場所で変数を宣言して初期化します。宣言と初期化の間にギャップを残さないでください。また、必要のない名前空間を汚染しないでください。

于 2009-01-02T16:18:10.217 に答える
227

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

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

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

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

于 2009-01-02T16:25:15.507 に答える
66

それは言語と正確な用途に依存します。たとえば、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変数があります。

于 2009-01-02T16:16:53.200 に答える
35

以下は私が書いて .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);
}

これは、 CILがコードにレンダリングされたときに.NET Reflectorから取得したものです。

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. 両者のパフォーマンスには非常に大きな違いがあります。より大きなスコープで何が意図されているかを理解できないため、コンパイラがコードを最適化できない場合がたくさんあります。しかし、基本的なことはほとんど最適化できます。

于 2010-01-13T11:52:39.503 に答える
23

これは 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...
于 2009-01-02T16:20:15.550 に答える
16

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

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 つのコードの main () を逆アセンブルした結果が次のとおりです。

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

2度

   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 

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

于 2015-10-10T14:24:23.117 に答える
13

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

于 2009-01-02T16:11:26.957 に答える
11

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

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

これは依然としてループのスコープに制限さintermediateResultれますが、各反復中に再宣言されません。

于 2009-01-02T16:18:13.650 に答える
6

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

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

于 2009-01-02T16:16:59.113 に答える
5

同僚は最初の形式を好み、それが最適化であると言い、宣言を再利用することを好みます。

私は 2 番目の方が好きです (そして、同僚を説得しようとします! ;-))、それを読んで:

  • 変数のスコープを必要な場所に縮小します。これは良いことです。
  • Java は、パフォーマンスに大きな違いがないほど十分に最適化します。IIRC、おそらく 2 番目の形式はさらに高速です。

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

于 2009-01-02T17:41:30.580 に答える
5

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

于 2009-01-02T16:14:05.360 に答える
5

原則として、変数はできるだけ内側のスコープで宣言します。したがって、ループの外側でintermediateResultを使用していない場合は、Bを使用します.

于 2009-01-02T16:15:15.387 に答える
5

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

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

したがって、バージョン a にはメリットがなく、コードについての推論がはるかに難しくなるため、私は際限なく悩まされます...

于 2012-11-07T21:15:13.193 に答える
5

ええと、いつでもそのためのスコープを作成できます。

{ //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 回宣言するだけで、ループを抜けると変数は消滅します。

于 2011-10-23T12:39:49.273 に答える
4

コンパイラに依存すると思いますので、一般的な答えを出すのは難しいです。

于 2009-01-02T16:08:53.077 に答える
4

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

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

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

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

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

次に、単一の参照のみを作成し、毎回新しいオブジェクトを割り当てます。確かに、範囲外になるまでにはもう少し時間がかかるかもしれませんが、対処する必要があるぶら下がっている参照は 1 つだけです。

于 2010-01-16T06:01:16.510 に答える
3

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

  • 変数の型が単純な場合(int、double、...)、バリアントb (内部) を好みます。
    理由:変数のスコープを縮小しています。

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

于 2014-02-05T06:03:24.560 に答える
1

誰かが興味を持っている場合は、Node 4.0.0 で JS をテストしました。ループの外側で宣言すると、試行ごとに 1 億回のループ反復で 1000 回の試行を超えると、平均で約 0.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)
于 2016-04-04T20:32:19.887 に答える
1

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

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 秒かかりました。

于 2015-04-20T12:29:41.780 に答える
0

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

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

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

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

于 2015-03-06T03:38:12.987 に答える
0

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」は特にソースライブラリに近い作業でメモリ蓄積に対して脆弱であると考えています.Linuxの「Valgrind」ツール、特にサブツール「Helgrind」を使用して確認できます。

于 2010-05-11T14:28:54.897 に答える
-1

コンパイラが十分に賢いとわかっていても、それに頼りたくないので、a) バリアントを使用します。

b) バリアントは、ループ本体の後で中間結果を使用不可にする必要がある場合にのみ意味があります。でも、そんな絶望的な状況は、どう考えても想像できない……。

編集: Jon Skeetは、ループ内の変数宣言が実際のセマンティックの違いを生む可能性があることを示して、非常に良い点を指摘しました。

于 2009-01-02T16:13:07.390 に答える