Foreach 루프 및 변수 초기화


11

이 두 버전의 코드간에 차이점이 있습니까?

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'
}

아니면 컴파일러가 신경 쓰지 않습니까? 차이점에 대해 말할 때 성능 및 메모리 사용 측면에서 의미합니다. .. 또는 기본적으로 차이점이 있습니까? 아니면 컴파일 후 두 코드가 동일한 코드입니까?


6
두 가지를 컴파일하고 바이트 코드 출력을 보셨습니까?

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

따라서 귀하의 질문에 대답하기 위해 : 컴파일러는 변수 선언을 최적화하고 두 변형을 동일하게 렌더링합니다.

내 이해를 위해 .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내가 대해 질문했다 동일한 일을 확인되지 않았 음. 결과 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접근법은 더 많은 지역 변수를 생성하고 추가 분기가 필요했습니다. 본질적으로 처음에는 루프의 끝으로 점프하여 열거의 첫 번째 반복을 얻은 다음 루프의 거의 맨 위로 이동하여 루프 코드를 실행합니다. 그런 다음 예상대로 계속 반복됩니다.

그러나 for및 foreach구문 을 사용하여 발생하는 분기 차이를 넘어선 선언 위치에 따라 IL 에는 차이 가 없었int i 습니다. 그래서 우리는 여전히 두 가지 접근법이 동등합니다.

사례 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 릴리스 모드를 대상으로하는 프로젝트를 다시 작성했습니다.

그리고이 세 가지 경우 모두에서 생성 된 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/…
— Andy

1
@ 앤디-링크 주셔서 감사합니다! foreach루프를 사용하여 생성 된 출력을 확인하고 대상 .NET 버전도 확인했습니다.

0

사용하는 컴파일러에 따라 (C #에 둘 이상이 있는지조차 알 수 없음) 코드는 프로그램으로 전환되기 전에 최적화됩니다. 좋은 컴파일러는 매번 같은 값을 다른 값으로 다시 초기화하고 메모리 공간을 효율적으로 관리한다는 것을 알 수 있습니다.

매번 같은 변수를 상수로 초기화했다면 컴파일러도 마찬가지로 루프 전에 변수를 초기화 하고 참조합니다.

그것은 컴파일러가 얼마나 잘 작성되었는지에 달려 있지만 코딩 표준에 관한 한 변수는 항상 가능한 범위 가 가장 적어야합니다 . 루프 안에서 선언하는 것은 내가 항상 배운 것입니다.


3
마지막 단락이 참인지 아닌지는 두 가지에 달려 있습니다. 자체 프로그램의 고유 한 컨텍스트 내에서 변수의 범위를 최소화하는 것의 중요성과 실제로 여러 할당을 최적화하는지 여부에 대한 컴파일러 내부 지식.
— Robert Harvey

그리고 바이트 코드를 기계 언어로 추가 변환하는 런타임이 있습니다. 여기에서 컴파일러 최적화와 같은 여러 가지 동일한 최적화도 수행됩니다.
— Erik Eidt

-2

먼저 내부 루프를 선언하고 초기화하므로 루프 루프마다 루프 내부에서 "i"가 다시 초기화됩니다. 두 번째로 루프 외부에서만 선언합니다.


1
이것은 2 년 전에 게시 된 최고의 답변에서 만들어지고 설명 된 포인트 이상의 실질적인 것을 제공하지 않는 것 같습니다
— gnat

2
답변 해 주셔서 감사합니다. 그러나 새로운 측면에 대해서는 인정 되지 않으며 , 최고 등급의 답변은 아직 자세히 다루지 않습니다.
— CharonX
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.