[c#] C #에서 ‘for’및 ‘foreach’제어 구조의 성능 차이

더 나은 성능을 제공하는 코드 스 니펫은 무엇입니까? 아래 코드 세그먼트는 C #으로 작성되었습니다.

1.

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2.

foreach(MyType current in list)
{
    current.DoSomething();
}



답변

음, 부분적으로 정확한 유형에 따라 다릅니다 list. 또한 사용중인 정확한 CLR에 따라 다릅니다.

어떤 식 으로든 중요한지 여부는 루프에서 실제 작업을 수행하는지 여부에 따라 다릅니다. 거의 모두 경우에 성능 차이는 크지 않지만 가독성의 차이는 foreach루프를 선호합니다 .

개인적으로 LINQ를 사용하여 “if”도 피할 것입니다.

foreach (var item in list.Where(condition))
{
}

편집 : List<T>with 를 반복 foreach하면 for루프 와 동일한 코드 가 생성 된다고 주장하는 사람들을 위해 다음과 같은 증거가 있습니다.

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

IL 생성 :

.method private hidebysig static void  IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
  IL_0006:  stloc.1
  .try
  {
    IL_0007:  br.s       IL_0017
    IL_0009:  ldloca.s   V_1
    IL_000b:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
    IL_0010:  stloc.0
    IL_0011:  ldloc.0
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_0017:  ldloca.s   V_1
    IL_0019:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
    IL_001e:  brtrue.s   IL_0009
    IL_0020:  leave.s    IL_0030
  }  // end .try
  finally
  {
    IL_0022:  ldloca.s   V_1
    IL_0024:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

컴파일러는 배열을 다르게 처리 하여 foreach루프를 기본적으로 루프 로 변환 for하지만 List<T>. 다음은 배열에 해당하는 코드입니다.

static void IterateOverArray(object[] array)
{
    foreach (object o in array)
    {
        Console.WriteLine(o);
    }
}

// Compiles into...

.method private hidebysig static void  IterateOverArray(object[] 'array') cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init (object V_0,
           object[] V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.2
  IL_0004:  br.s       IL_0014
  IL_0006:  ldloc.1
  IL_0007:  ldloc.2
  IL_0008:  ldelem.ref
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ldloc.2
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.2
  IL_0014:  ldloc.2
  IL_0015:  ldloc.1
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0006
  IL_001a:  ret
} // end of method Test::IterateOverArray

흥미롭게도 어디에서나 C # 3 사양에 문서화되어있는 것을 찾을 수 없습니다.


답변

for루프는이 약 해당 코드로 컴파일됩니다 :

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

어디로 foreach루프가이 거의 동등한 코드로 컴파일됩니다 :

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

보시다시피 열거자가 구현되는 방식과 목록 인덱서가 구현되는 방식에 따라 달라집니다. 배열을 기반으로 한 유형의 열거자는 일반적으로 다음과 같이 작성됩니다.

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

보시다시피이 경우에는 큰 차이가 없지만 연결된 목록의 열거자는 다음과 같이 보일 것입니다.

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

에서 .NET 당신은 당신이 연결 목록에 루프 당신을 할 수 없을 것입니다 그래서 LinkedList의 <T는> 클래스도, 인덱서가 발생하지 않는 것을 발견 할 것이다; 하지만 가능하다면 인덱서는 다음과 같이 작성해야합니다.

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

보시다시피 루프에서 이것을 여러 번 호출하는 것은 목록의 위치를 ​​기억할 수있는 열거자를 사용하는 것보다 훨씬 느립니다.


답변

반 검증하기 쉬운 테스트입니다. 나는 단지보기 위해 작은 테스트를했다. 다음은 코드입니다.

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    for (int i = 0; i < 10000000; i++)
    {
        intList.Add(i);
    }

    DateTime timeStarted = DateTime.Now;
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    TimeSpan finished = DateTime.Now - timeStarted;

    Console.WriteLine(finished.TotalMilliseconds.ToString());
    Console.Read();

}

다음은 foreach 섹션입니다.

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

나는 foreach 문으로의 대체 경우 – foreach는 20 밀리 초 빨랐다 – 일관 . foreach는 135-139ms이고 foreach는 113-119ms입니다. 나는 여러 번 앞뒤로 바꿨고 방금 시작된 프로세스가 아닌지 확인했습니다.

그러나 foo 및 if 문을 제거하면 for가 30ms 빨라졌습니다 (foreach는 88ms이고 for는 59ms였습니다). 둘 다 빈 껍질이었습니다. foreach가 실제로 변수를 증가시키는 변수를 전달했다고 가정합니다. 내가 추가하면

int foo = intList[i];

그러면 for는 약 30ms 느려집니다. 나는 이것이 foo를 만들고 배열에서 변수를 잡고 foo에 할당하는 것과 관련이 있다고 가정하고 있습니다. intList [i]에 액세스하면 해당 패널티가 없습니다.

솔직히 말해서 .. 나는 foreach가 모든 상황에서 약간 느려질 것으로 예상했지만 대부분의 응용 프로그램에서 중요하지는 않습니다.

편집 : 여기 Jons 제안을 사용하는 새 코드가 있습니다 (134217728은 System.OutOfMemory 예외가 발생하기 전에 가질 수있는 가장 큰 정수입니다) :

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    Console.WriteLine("Generating data.");
    for (int i = 0; i < 134217728 ; i++)
    {
        intList.Add(i);
    }

    Console.Write("Calculating for loop:\t\t");

    Stopwatch time = new Stopwatch();
    time.Start();
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Write("Calculating foreach loop:\t");
    time.Reset();
    time.Start();

    foreach (int i in intList)
    {
        int foo = i * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Read();
}

결과는 다음과 같습니다.

데이터 생성. for 루프 계산 : 2458ms foreach 루프 계산 : 2005ms

사물의 순서를 처리하는지 확인하기 위해 주변을 바꾸면 거의 동일한 결과가 나타납니다.


답변

참고 :이 답변은 C #에 인덱서가 없기 때문에 C #보다 Java에 더 많이 적용 LinkedLists되지만 일반적인 요점은 여전히 ​​유지하다고 생각합니다.

경우 list작업중인이를 될 일이 LinkedList, 인덱서 코드 (의 성능은 배열 스타일 에 접근) 훨씬 더 나쁜 사용하는 것보다 IEnumerator로부터를 foreach큰 목록에 대해.

LinkedList인덱서 구문 :을 사용하여 a 요소 10.000에 액세스 list[10000]하면 연결된 목록이 헤드 노드에서 시작 Next하여 올바른 개체에 도달 할 때까지 -pointer를 10,000 번 순회 합니다. 분명히 루프에서이 작업을 수행하면 다음을 얻을 수 있습니다.

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

호출하면 GetEnumerator(암시 적으로 forach-syntax 사용) IEnumerator헤드 노드에 대한 포인터가있는 객체를 얻게 됩니다. 를 호출 할 때마다 MoveNext해당 포인터는 다음과 같이 다음 노드로 이동합니다.

IEnumerator em = list.GetEnumerator();  // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.

보시다시피, LinkedLists 의 경우 배열 인덱서 메서드가 느리고 느려집니다. 루프 시간이 길어집니다 (동일한 헤드 포인터를 계속 반복해야 함). IEnumerable다만 일정한 시간에 작동 하는 반면 .

물론 Jon이 말했듯이 이것은 실제로 유형에 따라 달라집니다 list. list가 아니라 LinkedList배열이면 동작이 완전히 다릅니다.


답변

다른 사람들이 언급했듯이 성능이 실제로는별로 중요하지 않지만 foreach는 루프 의 IEnumerable/ IEnumerator사용으로 인해 항상 조금 느려질 것 입니다. 컴파일러는 구문을 해당 인터페이스의 호출로 변환하고 모든 단계에 대해 foreach 구문에서 함수 + 속성이 호출됩니다.

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

이것은 C #의 구문 확장에 해당합니다. MoveNext 및 Current의 구현에 따라 성능 영향이 어떻게 달라질 수 있는지 상상할 수 있습니다. 반면 배열 액세스에는 해당 종속성이 없습니다.


답변

“foreach 루프가 가독성을 위해 선호되어야한다”라는 충분한 주장을 읽은 후, 내 첫 반응이 “무엇”이라고 말할 수 있습니까? 일반적으로 가독성은 주관적이며 특히이 경우에는 더욱 그렇습니다. 프로그래밍에 대한 배경 지식이있는 사람 (실질적으로 Java 이전의 모든 언어)에게 for 루프는 foreach 루프보다 읽기가 훨씬 쉽습니다. 또한, foreach 루프가 더 읽기 쉽다고 주장하는 동일한 사람들은 코드를 읽고 유지하기 어렵게 만드는 linq 및 기타 “기능”의 지지자이기도합니다.

성능에 미치는 영향에 대해서는 질문에 대한 답변을 참조하십시오 .

편집 : 인덱서가없는 C # (HashSet와 같은) 컬렉션이 있습니다. 이 컬렉션에서 foreach 는 반복하는 유일한 방법 이며 .NET 용 으로 사용해야한다고 생각하는 유일한 경우 입니다 .


답변

두 루프의 속도를 테스트 할 때 쉽게 놓칠 수있는 흥미로운 사실이 있습니다. 디버그 모드를 사용하면 컴파일러가 기본 설정을 사용하여 코드를 최적화 할 수 없습니다.

이로 인해 foreach가 디버그 모드보다 빠르다는 흥미로운 결과가 나왔습니다. for는 릴리스 모드에서 foreach보다 빠르지 않습니다. 분명히 컴파일러는 여러 메서드 호출을 손상시키는 foreach 루프보다 for 루프를 최적화하는 더 좋은 방법을 가지고 있습니다. 그런데 for 루프는 기본적으로 CPU 자체에 의해 최적화 될 수도 있습니다.