C #에서 'for'및 'foreach'제어 구조의 성능 차이


105

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

1.

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

2.

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

31
나는 그것이 정말로 중요하지 않다고 생각합니다. 성능 문제가있는 경우에는 이것이 거의 확실하지 않습니다. 질문하면 안된다는 게 아니에요 ...
darasd

2
당신의 앱이 성능이 매우 중요하지 않다면 나는 이것에 대해 걱정하지 않을 것입니다. 깨끗하고 쉽게 이해할 수있는 코드를 갖는 것이 훨씬 좋습니다.
Fortyrunner

2
여기에있는 답변 중 일부가 단순히 반복기의 개념이 두뇌 어디에도 없기 때문에 열거 자나 포인터에 대한 개념이없는 사람들이 게시 한 것 같아 걱정됩니다.
Ed James

3
두 번째 코드는 컴파일되지 않습니다. System.Object에는 'value'라는 멤버가 없습니다 (정말 사악하고 확장 메서드로 정의하고 대리자를 비교하지 않는 한). foreach를 강력하게 입력하십시오.
Trillian

1
의 유형에 list실제로 count대신 멤버 가 없으면 첫 번째 코드도 컴파일되지 않습니다 Count.
Jon Skeet

답변:


130

음, 부분적으로 정확한 유형에 따라 다릅니다 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 사양에 문서화되어있는 것을 찾을 수 없습니다.


흥미롭지 않은 Jon, 위의 List <T> 시나리오는 ... 다른 컬렉션에도 적용됩니까? 또한, 당신은 이것을 어떻게 알았습니까 (어떤 악의가 의도되지 않은 상태에서) ... ..에서와 같이 이전 에이 질문에 대답하려고 시도하는 동안 문자 그대로 이것을 우연히 발견 했습니까? 이 : 그래서 ... / 임의 비밀
Pure.Krome

5
한동안 어레이 최적화에 대해 알고있었습니다. 어레이는 "핵심"종류의 컬렉션입니다. C # 컴파일러는 이미 이들을 깊이 인식하고 있으므로 다르게 처리하는 것이 좋습니다. 컴파일러는에 대한 특별한 지식이 없습니다 List<T>.
Jon Skeet

건배 :) 그리고 그래 ... 배열은 내가 uni ..에서 몇 년 전에 배운 첫 번째 컬렉션 개념이었습니다. 그래서 컴파일러가 가장 원시적 인 유형 중 하나를 처리하기에 충분히 똑똑하다는 것을 알 수 있습니다. 수집. 다시 건배!
Pure.Krome

3
@JonSkeet 목록 반복기를 최적화하면 반복 중에 목록이 수정 될 때 동작이 변경됩니다. 수정하면 예외가 손실됩니다. 최적화는 여전히 가능하지만 수정이 발생하지 않는지 확인해야합니다 (다른 스레드 포함).
Craig Gidney

5
@VeeKeyBee : 2004 년에 Microsoft가 말했습니다. a) 상황이 변합니다. 나) 작업이 수행되어야 할 것이다 작은 이 중요 할 때까지 반복 될 때마다 작업의 양. 참고 foreach배열 이상에 해당 for어쨌든. 항상 가독성을 우선으로 코딩 한 다음 측정 가능한 성능 이점을 제공 한다는 증거 가 있을 때만 마이크로 최적화하십시오 .
Jon Skeet

15

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

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


12

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

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

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


6
DateTime.Now보다 스톱워치를 사용하는 것이 더 낫습니다. 솔직히 말해서 그렇게 빨리 달리는 것은 믿지 않을 것입니다.
Jon Skeet

8
'for'가 각 반복 조건을 평가하기 때문에 foreach 루프가 더 빠르게 실행됩니다. 예제의 경우 하나의 추가 메서드 호출 (list.count를 가져 오기 위해)이 필요합니다. 간단히 말해서 두 개의 다른 코드 조각을 벤치마킹하고 있으므로 이상한 결과가 발생합니다. 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... '및'for '루프는 예상대로 항상 더 빠르게 실행됩니다!
AR

1
컴파일 후, for와 foreach는 프리미티브로 작업 할 때 정확히 동일한 것으로 최적화됩니다. List <T>를 소개 할 때까지는 속도가 크게 다릅니다.
앤서니 러셀

9

참고 :이 답변은 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배열이면 동작이 완전히 다릅니다.


4
.NET의 LinkedList에는 인덱서가 없으므로 실제로 옵션이 아닙니다.
Jon Skeet

오, 그 문제가 해결되면 :-) LinkedList<T>MSDN 의 문서를 살펴보고 있으며 꽤 괜찮은 API를 가지고 있습니다. 가장 중요한 것은 get(int index)Java와 같은 메소드 가 없다는 것 입니다. 그래도 특정 .NET보다 느린 인덱서를 노출하는 다른 목록과 같은 데이터 구조에 대해 여전히 요점이 유지된다고 생각합니다 IEnumerator.
Tom Lokhorst

2

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

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

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


4
배열 액세스와 인덱서 액세스간에 차이가 있다는 것을 잊지 마십시오. 목록이 List<T>여기에 있으면 인덱서 호출의 히트 (아마도 인라인)가 있습니다. 베어 메탈 어레이 액세스와는 다릅니다.
Jon Skeet

매우 사실입니다! 이것은 또 다른 속성 실행이며 우리는 구현의 자비에 있습니다.
Charles Prakash Dasari

1

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

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

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


0

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

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


0

제공 한 예제에서 foreach루프 대신 루프 를 사용하는 것이 확실히 좋습니다 for.

표준 foreach구조는 for-loop루프가 풀리지 않는 한 (단계 당 1.0 사이클) 단순 (단계 당 2 사이클 )보다 빠를 수 있습니다 (단계 당 1,5 사이클 ).

일상 코드 그래서, 성능은 더 복잡한 사용하는 이유없는 for, while또는 do-while구조를.

이 링크를 확인하십시오 : http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
        Method         List<int>  int[]  Ilist<int> onList<Int>  Ilist<int> on int[] 
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
 Time (ms)             23,80      17,56  92,33                   86,90               
 Transfer rate (GB/s)  2,82       3,82   0,73                    0,77                
 % Max                 25,2%      34,1%  6,5%                    6,9%                
 Cycles / read         3,97       2,93   15,41                   14,50               
 Reads / iteration     16         16     16                      16                  
 Cycles / iteration    63,5       46,9   246,5                   232,0               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


4
링크 한 코드 프로젝트 기사를 다시 읽을 수 있습니다. 흥미로운 기사이지만 귀하의 게시물과는 정반대입니다. 또한 다시 만든 테이블은 배열 및 목록에 직접 액세스하거나 해당 IList 인터페이스를 통해 액세스하는 성능을 측정합니다. 질문과 관련이 없습니다. :)
Paul Walls

0

Deep .NET 에서 읽을 수 있습니다 -파트 1 반복

.NET 소스 코드에서 디스 어셈블리까지의 결과 (첫 번째 초기화없이)를 다룹니다.

예를 들어-foreach 루프를 사용한 배열 반복 : 여기에 이미지 설명 입력

및-foreach 루프로 반복 나열 : 여기에 이미지 설명 입력

그리고 최종 결과 : 여기에 이미지 설명 입력

여기에 이미지 설명 입력

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.