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


312

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

どちらが、より良いですかB

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

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


これは、Androidプラットフォーム用のJavaコードを記述するときに重要です。Googleは、タイムクリティカルなコードがforループの外でインクリメントする変数を宣言する場合、forループ内のように、その環境で毎回再宣言することをお勧めします。パフォーマンスの違いは、高価なアルゴリズムでは非常に顕著です。
AaronCarson、2015年

1
@AaronCarsonは、Googleによるこの提案へのリンクを提供していただけませんか
Vitaly Zinchenko

回答:


256

どちらが、より良いですかB

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

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


5
Doubleの代わりに、Stringを処理する場合でも、ケース "b"の方が優れていますか?
Antoops 2014

3
@Antoops-はい、bは宣言されている変数のデータ型とは何の関係もない理由でより優れています。文字列ではなぜ違うのですか?
Daniel Earwicker 2015

215

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

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

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

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


12
プロファイリングの結果を投稿しようとしたところ、私は負けました。多かれ少なかれ同じ結果になりました。もちろん、Bの方が速いので、Bに賭ける必要があったとしたら、Aだと思っていたでしょう。
Mark Davidson、

14
それほど驚くべきことではありません。変数がループに対してローカルである場合、反復のたびに変数を保持する必要がないため、レジスタにとどまることができます。

142
+1は、OPが自分自身を構成する可能性がある意見/理論だけでなく、実際にテストするためのものです。
MGOwen

3
正直に言って@GoodPerson、私はそれが行われることを望みます。私はこのテストを自分のマシンで50,000,000〜100,000,000回の反復で約10回実行し、ほぼ同じコード(統計を実行したい人と共有したい)を使用しました。答えは、通常どちらか一方の方法で、通常は900ミリ秒(50M回を超える反復)のマージンで分割されましたが、それほど多くはありません。私の最初の考えは、それが「ノイズ」になるだろうということですが、少しずつ傾くかもしれません。この取り組みは、私にとっては純粋に学術的なもののようです(ほとんどの実際のアプリケーションの場合)。とにかく結果を確認したいのですが;)
javatarz 2013年

3
設定を文書化せずにテスト結果を表示することは価値がありません。これは、両方のコードフラグメントが同一のバイトコードを生成するこの場合に特に当てはまるため、測定された差はテスト条件が不十分であることを示しているにすぎません。
ホルガー

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変数を持っていることです。


3
例B(元の質問)では、実際に毎回新しい変数が作成されますか?スタックの目には何が起こっていますか?
Royi Namir

@ジョン、それはC#1.0のバグでしたか?理想的Outerには9にすべきではありませんか?
nawfal 14

@nawfal:どういう意味かわかりません。ラムダ式は1.0にはありませんでした...そして、アウター 9 です。どういうバグですか?
Jon Skeet、2014

@nawfal:私の要点は、C#1.0には、ループ内で変数を宣言することと、変数を外側で宣言すること(両方がコンパイルされていると仮定)の違いを区別できる言語機能がなかったということです。C#2.0で変更されました。バグなし。
Jon Skeet、2014

@JonSkeetああ、そうだ、私は今あなたを手に入れました。1.0ではそのような変数を閉じることができないという事実を完全に見落としました。:)
nawfal 2014

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


int j = 0 for(; j <0x3e8; j ++)このように宣言すると、両方の変数の時間を一度に宣言します。2)割り当ては、他のすべてのオプションよりも重要です。3)したがって、ベストプラクティスルールは、反復の範囲外の宣言です。
luka

24

これは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...

1
私は何年もVB.NETを使用していて、これに遭遇していませんでした。
ChrisA、

12
はい、実際にこれを理解するのは不愉快です。
Michael Haren、

ここではポール・ヴィックから、この程度の基準は次のとおりです。panopticoncentral.net/archive/2006/03/28/11552.aspx
ferventcoder

1
@eschneider @ferventcoder残念ながら、@ PaulVは彼の古いブログ投稿削除することを決定したため、これは現在デッドリンクです。
Mark Hurd、

うん、最近これに出くわした。この...上のいくつかの公式ドキュメントを探していた
エリック・シュナイダー

15

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

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つのコードが同じものを生成することの証明ではありませんか?


3
ええ、あなたがこれをしたのはクールですが、これは言語/コンパイラの依存関係について人々が言っ​​ていたことに戻ります。JITやインタプリタ言語のパフォーマンスにどのような影響があるのでしょうか。
user137717 16

12

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


ええ、でもそれはそれほど多くありません。forループを1億回実行する簡単なテストを実行したところ、ループの外側で宣言することを支持する最大の違いは8ミリ秒であることがわかりました。通常は3〜4に似ていて、ループの外側でWORSE(最大4ミリ秒)を実行することを宣言することもありましたが、それは一般的ではありませんでした。
user137717 16

11

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

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

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


12
概念的には、変数を反復ごとに個別にではなく、ループの期間中存続させたいですか?滅多にしない。特に意図がない限り、意図をできるだけ明確に示すコードを記述します。
Jon Skeet、

4
ああ、いい妥協、これは考えたことはありませんでした。IMO、コードは少し視覚的に「明確」になります)
Rabarberski

2
@ジョン-私はOPが中間値で実際に何をしているのかわかりません。検討する価値のあるオプションだと思いました。
トリプティク

6

私の意見では、bはより良い構造です。aでは、ループが終了すると、intermediateResultの最後の値が残ります。

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


sticks around after your loop is finished-これはPythonのような言語では問題ではありませんが、バインドされた名前は関数が終了するまで残ります。
new123456 2011年

@ new123456:質問いくぶん一般的尋ねられたとしても、OPはJavaの詳細を尋ねました。多くのC派生言語には、ブロックレベルのスコープがあります。C、C ++、Perl(myキーワードを使用)、C#、およびJavaは、私が使用した5という名前です。
Powerlord、2011年

私は知っています-それは批判ではなく観察でした。
new123456 2011年

5

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


5

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


5

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

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

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

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


5

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

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

したがって、バージョンを使用すると、何のメリットもないので、私を困らせることはありません。また、コードについて推論するのがはるかに困難になります...


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


4

ループ内で変数を宣言すると、メモリを浪費しているといつも思っていました。このようなものがあれば:

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

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

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

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

次に、1つの参照を作成し、そのたびに新しいオブジェクトを割り当てます。確かに、スコープから外れるまで少し時間がかかる場合がありますが、処理する必要のある参照は1つだけです。


3
参照が「for」ループ内で宣言されていても、新しい参照は各オブジェクトに割り当てられません。両方の場合:1) 'o'はローカル変数であり、関数の開始時にスタックスペースが1回割り当てられます。2)各反復で新しいオブジェクトが作成されます。したがって、パフォーマンスに違いはありません。コードの編成、可読性、保守性については、ループ内で参照を宣言する方が適切です。
Ajoy Bhatia、2010年

1
Javaについて話すことはできませんが、.NETでは、最初の例では、オブジェクトごとに参照が「割り当て」られていません。スタックには、その(メソッドへの)ローカル変数のエントリが1つあります。あなたの例では、作成されたILは同じです。
Jesse C. Slicer、

3

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


3

私の実践は以下の通りです:

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

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


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


2
それでは、最適化されていないコンパイルをデバッグしていたに違いありませんね。
Tomasz Przychodzki

int j = 0 for(; j <0x3e8; j ++)このように宣言すると、両方の変数の時間を一度に宣言します。2)割り当ては、他のすべてのオプションよりも重要です。3)したがって、ベストプラクティスルールは、反復の範囲外の宣言です。
luka

1

興味のある方は、Node 4.0.0でJSをテストしました。ループの外側で宣言すると、1回の試行あたり1億回のループ反復で、1000回を超える試行で平均して約0.5ミリ秒のパフォーマンス改善が得られました。だから私は先に進んで、最も読みやすく保守しやすい方法であるB imoでそれを書くつもりです。私はコードをいじくり回しましたが、今はパフォーマンスノードノードモジュールを使用しました。これがコードです:

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

A)は、B)よりも安全な賭けです。

お気に入り

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

面白い質問ですね。私の経験から、コードについてこの問題を議論するときに検討すべき究極の質問があります:

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

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

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


1
笑私

0

これはより良い形です

double intermediateResult;
int i = byte.MinValue;

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

1)この方法で宣言されるのは、両方の変数の時間であり、それぞれのサイクルではありません。2)割り当ては、他のすべてのオプションよりも重要です。3)したがって、ベストプラクティスルールは、反復の範囲外の宣言です。


0

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

アセンブラー出力によると、差はゼロです。


0

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

結論:このような場合、パフォーマンスに違いはありません

外ループケース

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の逆コンパイラでコンパイルされたファイルを確認しましたが、どちらの場合も同じです Test.class

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

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

外ループケース

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に割り当てられたiintermediateResultして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);
}

-1

私のコンパイラが十分に賢いことを知っていても、私はそれに依存するのは好きではなく、a)バリアントを使用します。

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

編集:Jon Skeetは非常に良い点を述べ、ループ内の変数宣言が実際の意味上の違いをもたらすことができることを示しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.