Різниця продуктивності для структур управління 'для' та 'foreach' в C #


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
Мене хвилює те, що деякі відповіді на них, схоже, розміщені людьми, які просто не мають поняття ітератора ніде в своєму головному мозку, а отже, немає поняття перелічувачів чи покажчиків.
Ед Джеймс

3
Цей 2-й код не збирається. У System.Object немає члена, який називається "value" (якщо ви справді не злий, не визначили це як метод розширення і порівнюєте делегатів). Сильно введіть свій підказку.
Trillian

1
Перший код також не буде скомпільований, якщо тільки тип listдійсно не має countчлена замість Count.
Джон Скіт

Відповіді:


130

Ну, це частково залежить від точного типу list. Це також залежатиме від точного CLR, який ви використовуєте.

Будь-яке значення це чи ні, залежатиме від того, чи виконуєте ви якусь реальну роботу в циклі. Майже у всіх випадках різниця у продуктивності не буде суттєвою, але різниця у читанні сприяє foreachциклу.

Я особисто використовую LINQ, щоб уникнути "якщо" також:

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

EDIT: Для тих з вас , хто стверджує , що Перебір List<T>з foreachвиробляє один і той же код , як forпетля, ось доказ того, що він не робить:

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

Випускає ІЛ:

.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 ніде ...


Нецікавий Джон, сценарій зі списком <T> вище ... це стосується і інших колекцій? Крім того, звідки ви це знали (без жодної злоби, призначеної ні для чого) ... як у .. чи буквально ви натрапили на це, намагаючись відповісти на це питання, раніше деякий час тому? Це так ... випадково / таємно :)
Pure.Krome

5
Я деякий час знав про оптимізацію масивів - масиви - це "ядро" колекції; компілятор C # вже глибоко усвідомлює їх, тому має сенс ставитися до них по-різному. Компілятор не має (і не повинен) жодних спеціальних знань List<T>.
Джон Скіт

Ура :) та так ... масив був першою концепцією колекції, яку я навчав років і роки тому в університеті .. тож було б зрозуміло, що компілятор досить розумний, щоб мати справу з одним із (якщо не з) найпримітивнішим типом колекція. ура знову!
Pure.Krome

3
@JonSkeet Оптимізація ітератора списку не змінює поведінку, коли список змінюється під час ітерації. Ви втрачаєте виняток, якщо його змінено. Ще можна оптимізувати, але вимагає перевірити, чи не відбудуться модифікації (в тому числі на інших потоках).
Крейг Гідні

5
@VeeKeyBee: Так сказав Microsoft у 2004 році. A) все змінюється; б) робота повинна проводити невеликі обсяги роботи над кожною ітерацією, щоб це було суттєвим. Зауважте, що foreachнад масивом рівнозначно for. Завжди кодуйте читабельність спочатку, а потім лише мікрооптимізуйте, коли у вас є докази того, що це дає корисну ефективність.
Джон Скіт

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 (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

Коли я замінив на форш, - передбачення було на 20 мілісекунд швидше - послідовно . Форт був 135-139 мс, а передбачення - 113-119 мс. Я кілька разів мінявся вперед і назад, переконуючись, що це не якийсь процес, який просто запустився.

Однак, коли я видалив foo та if-заяву, формат був швидшим на 30 мс (foreach склав 88ms, а для 59ms). Вони обоє були порожніми снарядами. Я припускаю, що foreach фактично передав змінну, де як for просто нарощував змінну. Якби я додав

int foo = intList[i];

Тоді for стає повільним приблизно на 30 мс. Я припускаю, що це було пов'язано з створенням foo та захопленням змінної у масиві та віднесенням до foo. Якщо ви просто отримаєте доступ до intList [i], ви не маєте цього штрафу.

Чесно кажучи .. Я очікував, що передбачення буде трохи повільнішим за будь-яких обставин, але недостатньо, щоб мати значення в більшості застосувань.

редагувати: ось новий код за допомогою пропозицій 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();
}

І ось результати:

Генерування даних. Розрахунок для циклу: 2458ms Розрахунок циклу foreach: 2005ms

Поміняючи їх навколо, щоб побачити, чи стосується він порядок речей, дає ті ж результати (майже).


6
Краще використовувати секундомір, ніж DateTime.Now - і я б не повірив жодному швидкому пробігу, якщо чесно.
Джон Скіт

8
Ваші петлі передбачення працюють швидше, оскільки "для" оцінює умову кожної ітерації. Що стосується вашого прикладу, це викликає додатковий виклик методу (щоб отримати list.count) Коротше кажучи, ви орієнтуєте два різних фрагменти коду, отже, і ваші дивні результати. Спробуйте 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... ", а цикл" для "завжди працюватиме швидше, як очікувалося!
AR

1
Після компіляції, for і foreach оптимізуються до точно такого ж, що працює при примітивах. Це лише тоді, коли ви не введете Список <T>, що вони різняться (сильно) за швидкістю.
Ентоні Рассел

9

Примітка: ця відповідь більше стосується Java, ніж для C #, оскільки C # не має індексатора LinkedLists, але я думаю, загальна точка все-таки має місце.

Якщо listви працюєте з трапляється бути LinkedList, продуктивність індексатор-коду ( масив типу доступу) набагато гірше , ніж при використанні IEnumeratorз foreach, для великих списків.

Коли ви отримуєте доступ до елемента 10.000 у LinkedListвикористанні синтаксису індексатора:, list[10000]пов'язаний список розпочнеться з головного вузла і Nextперемістить -показник десять тисяч разів, поки не досягне правильного об'єкта. Очевидно, якщо ви зробите це в циклі, ви отримаєте:

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

Коли ви телефонуєте GetEnumerator(неявно використовуючи forach-синтакс), ви отримаєте 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справедливий працює в постійному часі.

Звичайно, як сказав Джон, це дійсно залежить від типу list, якщо listце не а LinkedList, а масив, поведінка зовсім інша.


4
LinkedList в .NET не має індексатора, тому це насправді не варіант.
Джон Скіт

О, добре, що вирішує цю проблему, тоді :-) Я просто переглядаю LinkedList<T>документи на MSDN, і він має досить пристойний API. Найголовніше, що у нього немає get(int index)методу, як у Java. Але я думаю, що точка все ще має місце для будь-якої іншої структури даних, подібної до списку, яка відкриває індексатор, який повільніше, ніж конкретний IEnumerator.
Том Лохорст

2

Як і інші люди згадували, хоча продуктивність насправді не має великого значення, передбачення завжди буде трохи повільніше через IEnumerable/ IEnumeratorвикористання в циклі. Компілятор переводить конструкцію у виклики цього інтерфейсу, і для кожного кроку в конструкції foreach викликається функція + властивість.

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

Це еквівалентне розширення конструкції в C #. Ви можете уявити, як вплив на продуктивність може змінюватися залежно від реалізації MoveNext та Current. Тоді як у доступі до масиву, ти не маєш цих залежностей.


4
Не забувайте, що існує різниця між доступом до масиву та доступом до індексатора. Якщо список List<T>тут, то все ще є хіт (можливо, вказівний) виклику індексатора. Це не так, як це доступ з чистого металевого масиву.
Джон Скіт

Дуже правильно! Це ще одне майнове виконання, і ми перебуваємо на милості реалізації.
Чарльз Пракаш Дасарі

1

Прочитавши достатньо аргументів про те, що "циклу передбачення слід віддати перевагу для читабельності", я можу сказати, що моя перша реакція була "що"? Читання, загалом, є суб'єктивним і, в даному конкретному випадку, навіть більше. Для тих, хто має досвід програмування (практично на кожній мові перед Java), петлі читати набагато простіше, ніж цикли передбачення. Крім того, ті ж люди, які стверджують, що петлі передбачення є більш читабельними, також є прихильниками linq та інших "функцій", які роблять код важким для читання та обслуговування, що підтверджує вищезазначене.

Про вплив на продуктивність дивіться відповідь на це питання.

EDIT: У C # (як HashSet) є колекції, які не мають індексатора. У цих колекціях, Еогеасп це єдиний спосіб ітерації , і це єдиний випадок , я думаю , що він повинен бути використаний в протягом для .


0

Є ще один цікавий факт, який можна легко пропустити при тестуванні швидкості обох циклів: Використання режиму налагодження не дозволяє компілятору оптимізувати код за допомогою налаштувань за замовчуванням.

Це призвело до цікавого результату, що передбачення швидше, ніж у режимі налагодження. Тоді як for ist швидше, ніж foreach у режимі випуску. Очевидно, у компілятора є кращі способи оптимізації циклу, ніж цикл foreach, який компрометує кілька викликів методів. Цикл for є, до речі, таким принциповим, що цілком можливо, що це навіть оптимізується самим процесором.


0

У наданому вами прикладі, безумовно, краще використовувати foreachцикл, а не forцикл.

Стандартна foreachконструкція може бути швидшою (1,5 циклу на крок), ніж проста for-loop(2 цикли на крок), якщо тільки цикл не був розкручений (1,0 циклу на крок).

Так що для повсякденного коду, продуктивність не є підставою для використання більш складних 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: введіть тут опис зображення

і кінцеві результати: введіть тут опис зображення

введіть тут опис зображення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.