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

그러나 forforeach구문 을 사용하여 발생하는 분기 차이를 넘어선 선언 위치에 따라 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.