Foreachループと変数の初期化


11

これらの2つのバージョンのコードに違いはありますか?

foreach (var thing in things)
{
    int i = thing.number;
    // code using 'i'
    // pay no attention to the uselessness of 'i'
}

int i;
foreach (var thing in things)
{
    i = thing.number;
    // code using 'i'
}

または、コンパイラは気にしませんか?違いについて言えば、パフォーマンスとメモリ使用量という意味です。..基本的には単に違いがありますか、コンパイル後に2つが同じコードになりますか?


6
2つをコンパイルしてバイトコード出力を見てみましたか?

4
@MichaelTバイトコード出力を比較する資格があるとは思わない..違いを見つけた場合、それが正確に何を意味するかを理解できるかどうかわからない。
Alternatex

4
同じ場合は、認定を受ける必要はありません。

1
@MichaelTコンパイラーが最適化できたかどうかを十分に推測するためには十分な資格が必要ですが、もしそうであれば、どのような条件下で最適化を行うことができますか。
ベンアーロンソン

@BenAaronsonの場合、その機能をくすぐるために重要な例を必要とします。

回答:


22

TL; DR -IL層での同等の例です。


DotNetFiddleを使用すると、結果のILを確認できるので、答えが非常にわかりやすくなります。

テストを高速化するために、ループ構造のわずかに異なるバリエーションを使用しました。私が使用した:

バリエーション1:

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        int x;
        int i;

        for(x=0; x<=2; x++)
        {
            i = x;
            Console.WriteLine(i);
        }
    }
}

バリエーション2:

        Console.WriteLine("Hello World");
        int x;

        for(x=0; x<=2; x++)
        {
            int i = x;
            Console.WriteLine(i);
        }

どちらの場合でも、コンパイルされたIL出力は同じものをレンダリングしました。

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  2
    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ldc.i4.0
    IL_000d:  stloc.0
    IL_000e:  br.s       IL_001f

    IL_0010:  nop
    IL_0011:  ldloc.0
    IL_0012:  stloc.1
    IL_0013:  ldloc.1
    IL_0014:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0019:  nop
    IL_001a:  nop
    IL_001b:  ldloc.0
    IL_001c:  ldc.i4.1
    IL_001d:  add
    IL_001e:  stloc.0
    IL_001f:  ldloc.0
    IL_0020:  ldc.i4.2
    IL_0021:  cgt
    IL_0023:  ldc.i4.0
    IL_0024:  ceq
    IL_0026:  stloc.2
    IL_0027:  ldloc.2
    IL_0028:  brtrue.s   IL_0010

    IL_002a:  ret
  } // end of method Program::Main

質問に答えるために、コンパイラは変数の宣言を最適化し、2つのバリエーションを同等にレンダリングします。

私の理解では、.NET ILコンパイラーはすべての変数宣言を関数の先頭に移動しますが、2と明確に述べた良いソースを見つけることができませんでした。この特定の例では、次のステートメントでそれらが上に移動したことがわかります。

    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)

ここで、比較をするのに少し強迫観念を抱いています...。

ケースA、すべての変数が上に移動しますか?

これをさらに掘り下げるために、次の機能をテストしました。

public static void Main()
{
    Console.WriteLine("Hello World");
    int x=5;

    if (x % 2==0) 
    { 
        int i = x; 
        Console.WriteLine(i); 
    }
    else 
    { 
        string j = x.ToString(); 
        Console.WriteLine(j); 
    } 
}

ここでの違いは、比較に基づいてint iまたはを宣言することstring jです。繰り返しますが、コンパイラーはすべてのローカル変数を関数2の先頭に移動します:

.locals init (int32 V_0,
         int32 V_1,
         string V_2,
         bool V_3)

私はそれがたとえことは興味深い見つけint i、この例で宣言されることはありません、それをサポートするためのコードがまだ生成されます。

ケースB:foreachではなくfor

foreach動作が異なることforと、質問されたのと同じことをチェックしていないことが指摘されました。そのため、これら2つのコードセクションを挿入して、結果のILを比較します。

int ループ外の宣言:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};
    int i;

    foreach(var thing in things)
    {
        i = thing;
        Console.WriteLine(i);
    }

int ループ内の宣言:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};

    foreach(var thing in things)
    {
        int i = thing;
        Console.WriteLine(i);
    }

結果として得られるforeachループのILは、ループを使用して生成されたILとは実際に異なりましたfor。具体的には、initブロックとループセクションが変更されました。

.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
         int32 V_1,
         int32 V_2,
         class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
         valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
         bool V_5)
...
.try
{
  IL_0045:  br.s       IL_005a

  IL_0047:  ldloca.s   V_4
  IL_0049:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
  IL_004e:  stloc.1
  IL_004f:  nop
  IL_0050:  ldloc.1
  IL_0051:  stloc.2
  IL_0052:  ldloc.2
  IL_0053:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0058:  nop
  IL_0059:  nop
  IL_005a:  ldloca.s   V_4
  IL_005c:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
  IL_0061:  stloc.s    V_5
  IL_0063:  ldloc.s    V_5
  IL_0065:  brtrue.s   IL_0047

  IL_0067:  leave.s    IL_0078

}  // end .try
finally
{
  IL_0069:  ldloca.s   V_4
  IL_006b:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
  IL_0071:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0076:  nop
  IL_0077:  endfinally
}  // end handler

このforeachアプローチでは、より多くのローカル変数が生成され、追加の分岐が必要になりました。基本的に、最初に列挙の最初の反復を取得するためにループの最後にジャンプし、ループコードを実行するためにループのほぼ先頭に戻ります。その後、期待どおりループし続けます。

しかし、forand foreach構文を使用することによって生じる分岐の違いを超えて、宣言が置かれた場所に基づいてILに違いはありませんint iでした。したがって、2つのアプローチは同等です。

ケースC:異なるコンパイラバージョンについてはどうですか?

1 に残されたコメントには、foreachを使用した変数アクセスとクロージャーの使用に関する警告に関するSO質問へのリンクがありました。その質問で本当に目を引いた部分は、.NET 4.5コンパイラが以前のバージョンのコンパイラとどのように機能するかに違いがあるかもしれないということでした。

そして、DotNetFiddlerサイトで私を失望させたのは、.NET 4.5とRoslynコンパイラのバージョンだけでした。そこで、Visual Studioのローカルインスタンスを作成し、コードのテストを開始しました。同じことを比較するために、.NET 4.5でローカルにビルドされたコードをDotNetFiddlerコードと比較しました。

私が注意した唯一の違いは、ローカルの初期化ブロックと変数の宣言にありました。ローカルコンパイラは、変数の命名においてもう少し具体的でした。

  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
           [1] int32 thing,
           [2] int32 i,
           [3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
           [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
           [5] bool CS$4$0001)

しかし、そのわずかな違いはありましたが、これまでのところ、とても良かったです。DotNetFiddlerコンパイラとローカルVSインスタンスが生成するものとの間で同等のIL出力がありました。

そのため、.NET 4、.NET 3.5、および.NET 3.5リリースモードをターゲットにしたプロジェクトを再構築しました。

そして、これらの追加の3つのケースすべてで、生成されたILは同等でした。ターゲットの.NETバージョンは、これらのサンプルで生成されたILには影響しませんでした。


この冒険を要約する と、コンパイラはプリミティブ型をどこで宣言するかを気にかけず、どちらの宣言方法でもメモリやパフォーマンスに影響はないと自信を持って言えると思います。そして、foror foreachループを使用するかどうかに関係なく当てはまります。

foreachループ内にクロージャーを組み込んだ別のケースを実行することを検討しました。しかし、あなたはプリミティブ型変数が宣言された場所の影響について尋ねたので、私はあなたが興味を持っているものを超えて深く掘り下げていたと思いました。先ほど触れたSOの質問には、foreach反復変数のクロージャー効果に関する優れた概要を提供する素晴らしい答えがあります。

1 ループ内の閉鎖に対処するSO質問への元のリンクを提供してくれたAndyに感謝しますforeach

2 ECMA-335仕様は、セクションI.12.3.2.2「ローカル変数と引数」でこれに対処していることに注意しください。結果のILを確認し、次に何が起こっているのかを明確にするためにセクションを読む必要がありました。チャットで指摘してくれたラチェットフリークに感謝します。


1
forとforeachの動作は同じではありません。また、ループ内にクロージャーがある場合に重要になる異なるコードが質問に含まれています。 stackoverflow.com/questions/14907987/…–
アンディ

1
@アンディ-リンクをありがとう!foreachループを使用して生成された出力をチェックし、ターゲットの.NETバージョンもチェックしました。

0

使用するコンパイラに応じて(C#に複数のコンパイラがあるかどうかさえわかりません)、プログラムに変換される前にコードが最適化されます。優れたコンパイラーは、毎回同じ変数を異なる値で再初期化し、そのためのメモリー空間を効率的に管理していることを確認します。

同じ変数を毎回定数に初期化していた場合、コンパイラーはループの前に同様に初期化して参照します。

それはすべて、コンパイラがどれだけうまく書かれているかに依存しますが、コーディング標準に関する限り、変数は常に可能な限り最小のスコープを持つべきです。したがって、ループ内で宣言することは、私が常に教えられてきたことです。


3
最後の段落が真であるかどうかは、次の2つの要素に依存します。独自のプログラムの固有のコンテキスト内で変数のスコープを最小化することの重要性、および実際に複数の割り当てを最適化するかどうかに関するコンパイラーの内部知識。
ロバートハーヴェイ

次に、ランタイムがあり、バイトコードを機械語にさらに変換します。これらの同じ最適化の多く(ここではコンパイラの最適化として説明します)も実行されます。
エリックエイド

-2

最初はループ内で宣言および初期化するだけなので、ループがループするたびにループ内で「i」が再初期化されます。次に、ループの外側でのみ宣言しています。


1
これは作られたポイントを超える大幅な提供の何にも思えるし、2年以上も前に投稿されたトップの答えで説明していません
ブヨ

2
答えてくれてありがとう、しかしそれは受け入れられた、最高評価の答えがまだ(詳細に)カバーしていない新しい側面を与えない。
CharonX
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.