Что быстрее? foreach vs. List.ForEach vs. for-loop

Original author: Dustin Campbell
  • Translation
Сегодня (прим. переводчика: т.е. 6 лет назад) я перебирал список List, используя конструкцию foreach, и чувствуя небольшое самодовольство, осознавая насколько это производительнее, того что было бы, попытайся я использовать ArrayList. Благодаря чуду Generic компилятор C# аккуратно избегает многочисленные упаковочные операции с помощью экземпляров System.Collections.Generic.IEnumerator вместо старых System.Collections.IEnumerator. Тогда я подумал: "действительно ли это самый быстрый способ?" По результатам расследования, получается, что, нет, это не самый быстрый способ.

NET Framework 2.0 (прим. переводчика: да, статья старая) полон маленьких вкусняшек, которые делают жизнь легче, а код элегантнее. Среди моих любимых, дополнительные методы для массивов и списков, такие как Action, Converter<TInput,TOutput> и
Predicate, которые принимают создаваемые делегаты. Судя по всему, я одержим ими. Эти методы действительно сияют, когда используются с анонимными методами и лямбда выражениями. В частности, я заинтересовался методом List.ForEach(Action). Я захотел узнать, ForEach медленнее, быстрее или примерно такой же по производительности как и стандартный Foreach?
Рассмотрим этот код:
long Sum(List<int> intList) { long result = 0; foreach (int i in intList) result += i; return result; }

Компилятор C# будет генерировать IL инструкцию для приведенного выше кода, который транслируется примерно так:

long Sum(List<int> intList)
{
  long result = 0;
  List<T>.Enumerator enumerator = intList.GetEnumerator();
  try
  {
    while (enumerator.MoveNext())
    {
      int i = enumerator.Current;
      result += i;
    }
  }
  finally
  {
    enumerator.Dispose();
  }
  return result;
}

По сути, компилятор C # генерирует два вызываемых метода за одну итерацию:
IEnumerator .MoveNext () и IEnumerator .Current. Использование структуры List.Enumerator (которая реализует IEnumerator) позволяет компилятору генерировать вызываемые IL инструкции вместо callvirt, чтобы обеспечить крошечный прирост скорости на нижнем уровне. 
Для разнообразия, рассмотрим этот код:
long Sum(List<int> intList) { long result = 0; intList.ForEach(delegate(int i) { result += i; }); result result; } Или тот же код, на использующий лямбда выражения: long Sum(List<int> intList) { long result = 0; intList.ForEach(i => result += i); return result; }


В результате, используя List.ForEach создаётся только один метод за итерацию, где Action<Т> любой делегат, который вы предоставляете. Он будет вызывать callvirt IL инструкцию, но два вызова медленнее, чем один callvirt. Итак, я ожидаю, что List.ForEach на самом деле будет быстрее.
Вооруженный моим предположениям, я создал небольшое консольного приложения, которое замеряло время суммирования всех элементов списка List для четырёх различных способов перебора:
  1. for (int i = 0; i < List.Count; i++)
    for (int i = 0; i < NUM_ITEMS; i++)
    foreach (int i in List)
    List.ForEach(delegate(int i) { result += i; });

    Первые, результаты я сгенерировал без оптимизации компилятора:
    image
    Эти результаты невероятны. Оказывается, что без оптимизации, List.ForEach даже быстрее, чем стандартные конструкции! Не меньший интерес представляет, что (без оптимизации компилятора) Foreach почти так же быстр, как стандартные циклы. Итак, если вы собираете приложение, без оптимизации компилятора, то List.ForEach является самым быстрым методом.
    Далее, я прогнал мои тесты с поддержкой оптимизации компилятора, чтобы получить результаты приближенные к "реальному миру":
    image
    Глядя на эти цифры, я находился под впечатлен от того, как эффективно компилятор оптимизирует циклы, чтобы получить более, чем 50% экономии. ForEach-петлям также удалось урезать около 20%. List.ForEach не оптимизируется так хорошо, но главное, что он по-прежнему обгоняет Foreach-петли со значительным отрывом. List.ForEach быстрее, чем стандартный Foreach.

    Замечу, что эти тесты проводились на ноутбуке Dell Inspiron 9400 с Core Duo T2400 и 2 Гб оперативной памяти. Если вы хотите прогнать эти тесты у себя, вы можете скачать консольное приложение здесь: ForEachTest.zip (3,82 Кб).
    И так как одна картинка стоит тысячи слов, я создал пару графиков, чтобы продемонстрировать различия в скорости тестов. На графиках показано 5 различных образцов, позволяющие перебирать 10000000 в 50000000 целых значений.

    Без оптимизации компилятора:
    image

    С оптимизацией компилятора:
    image

    И наконец: в то время как метод ForEach намного быстрее для перебора List, это не применимо для случаев с массивами. Метод Array.ForEach
    существует для одномерных массивов (или векторов) и он на самом деле медленнее, чем используемый стандартный foreach. Причина в том, что компилятор не генерирует код, использующий IEnumerator для foreach-петель через векторы. Вместо этого, он генерирует специальные инструкции IL, которые предназначены для работы с векторами. Таким образом, используя foreach на векторах, методы в итоге не вызываются, в то время как Array.ForEach еще требует один вызов для предоставленного делегата.

    Примечание переводчика:
    Т.к. для написания скриптов на Unity3D я использую C#, то я не смог устоять от соблазна проверки описанных выводов на практике. Я написал небольшой
    скрипт
    
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    public class testspeed : MonoBehaviour {
    
    	public enum LoopType {none, for_loop, for_loop_no_count, foreach_loop, ListintForEach}
    	public LoopType type;
    	public int Count=2;
    	public int iterationCount=1000;
    	private List<int> lint = new List<int>();
    	private int[] alint;
    	private long result;
    
    	void Start () {
    		for (int i = 0 ; i<Count; i++) {int r=UnityEngine.Random.Range(0,1000); lint.Add(r);}
    	}
    	
    	void Update () {
    		for (int j =0; j<iterationCount;j++)
    		{
    			result=0;
    			switch (type)
    			{
    			case LoopType.for_loop: for (int i = 0; i < lint.Count; i++) {result += lint[i];} break;
    			case LoopType.for_loop_no_count: for (int i = 0; i < Count; i++){result += lint[i];} break;
    			case LoopType.foreach_loop: foreach (int i in lint) {result += i;} break;
    			case LoopType.ListintForEach: lint.ForEach(i => result += i); break;
    			}
    		}
    
    
    	}
    }
    


    В результате, я заметил одно лукавство в приведённых выше данных, заключающееся в том, что нижней границей для таблицы выбрано 1000 членов списка. Если список содержит в себе больше 1000 элементов, то всё вышеописанное верно. Но у меня на практике такое такое встречается не часто, обычно список ограничивается десятками, ну может сотнями элементов.
    Поэтому я привожу свою таблицу:
    image
    Как видно, для небольших списков лучше всего использовать стандартный цикл с внешним счётчиком. По производительности ForEach проигрывает ему в разы.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 39

    +4
    Неужели есть в мире приложения в которых разница в скорости между for и foreach будет иметь хоть сколь-нибудь важное значение?
    А уж сравнивать задержка на 1, 10 или 100 проходах вообще странно. Естественно время, затраченное на инициализацию будет значительно влиять.
    Ну и конечно habrahabr.ru/post/192130/
      0
      Интересная статья, жаль не попалась мне раньше. Что впрочем не отменяет ценность этой статьи. В вашей ссылке написано, что List.ForEach показал худший результат. Здесь же видно, что для длинных списков его производительность такая же как у for-loop. Плюс её можно записывать более элегантно и использовать делегаты.
        0
        У foreach есть побочный эффект в виде создания объекта итератора, что на мобильных платформах может аукнуться при интенсивном использовании циклов.
          0
          Для List и прочих встроенных типов коллекций, энумераторы обхявлены как value types, поэтому их создание не несет существенных накладных расходов. Плюс, они создаются по штуке на цикл (а не итерацию).
            0
            Про value types не знал, если это так и итераторы создаются на стеке, тогда да проблем быть не должно.
        –1
        Вы бы сравнили с foreach С# 5 т.к. он разворачивается в чуть более ресурсоемкую конструкцию:
        {
            E e = ((C)(x)).GetEnumerator();
            try {
                while (e.MoveNext()) {
                    V v = (V)(T)e.Current;
                    embedded-statement
                }
            }
            finally {
                … // Dispose e
            }
        }

        вместо того как было:

        {
            E e = ((C)(x)).GetEnumerator();
            try {
                V v;
                while (e.MoveNext()) {
                    v = (V)(T)e.Current;
                    embedded-statement
                }
            }
            finally {
                … // Dispose e
            }
        }

        А всё из-за обезьян, которые не смогли осилить замыкание.
          0
          Ну это всё-таки перевод, а Я работаю на C# 3.2.4.
            0
            Почитал про foreach C# 5, и что-то мне кажется, что это фича, а не баг.
            Поэтому я ой не торопился бы про обезьян.
              0
              Ткните пальцем туда, где я написал что это баг. Эрик Липперт в своём блоге как раз и писал, что они это сделали из-за «баг репортов» тех самых обезьян, которые не понимают что такое замыкание.
                0
                Ясно. Лично я подумал, что про обезьян — это вы о разработчиках компилятора :)
                  +2
                  Все равно ваша логика непонятна. То, как сделано в C# 5 — это как раз так, как оно должно было быть с самого начала, при наличии замыканий. Но замыканий в С# 1.0 не было, поэтому сделали как проще — а когда их добавили в 2.0, то это решение пересмотрено не было.

                  «Обезьяны» тут совершенно ни при чем, т.к. нет каких-либо причин интуитивно предпочитать первое раскрытие второму — скорее наоборот, потому что нет никакого смысла в том, чтобы держать общую переменную для всего цикла — ну нет такого сценария, где хочется её захватить в замыкании, и увидеть изменение на следующем цикле.

                  К производительности это все не имеет никакого отношения, т.к. переменная не «выделяется» на каждой итерации цикла.
                    0
                    Было: поведение контринтуитивное и совершенно бесполезное для программистов. Хак компилятора наружу, протекшая абстракция.
                    Стало: поведение предполагаемое любым разумным человеком не читавшим описание foreach и удобное для программистов.

                    А вот почему вдруг новое поведение более ресурсоемкое… тут знаете надо оптимизатор точить, а не программистов обезьянами называть.
                      0
                      Во-первых, это breaking changes, во вторых — раньше оно работало так же как и for, в котором замыкание всё ещё есть.
                        0
                        Во-первых, это breaking changes

                        xkcd.com/1172

                        раньше оно работало так же как и for

                        А сейчас оно работает так же, как ForEach, что гораздо логичнее.
                        Может кто-то и использует регулярно for в своей работе, но я, к счастью, с такой необходимостью давно не встречался.
                          0
                          >>А сейчас оно работает так же, как ForEach, что гораздо логичнее.
                          Чтоб вы знали, List::ForEach сами MS считают злом (и намеренно не делали LINQ расширение) и успешно выпилили из Windows 8 этот метод из листа. Если хотите, могу скинуть сцылку на соответсвующую блогозапись Эрика Липперта.
                            0
                            Хорошо, вы меня вынудили таки почитать его статьи по этому поводу.

                            Притензии к ForEach у него исключительно семантические: LINQ не для сайдэффектов. Не могу с ним не согласиться, особенно в синтаксисе C#.

                            Про foreach же он высказывается весьма однозначно:
                            Also, it seems reasonable that the user of the foreach loop might think of there being a «fresh» loop variable every time, not just a fresh value in the same old variable. Since the foreach loop variable is not mutable by user code, this reinforces the idea that it is a succession of values, one per loop iteration, and not «really» the same variable over and over again.

                            Тут же указано (жирный шрифт) существенное отличие foreach от for.

                            Да, он приводит несколько причин для существования текущего поведения, но я готов с ними поспорить. Моим тезисом было бы ассоциированные foreach с ForEach, в не с for. Так, сделано, например, в scala.

                            Ну и да, immutable переменная при замыкании должна иметь поведение копирования значения.
                          –1
                          Нет, не работало.
                  +5
                  > ForEach-петлям
                  > петлям
                  Циклы же…
                    +1
                    тесты TestListFor и TestArrayFor считают сумму индексов. Остальные тесты — считают сумму элементов списка/массива.
                    static void TestArrayFor()
                    {
                        long totalValue = 0;
                        for (int i = 0; i < g_IntArray.Length; i++)
                            totalValue += i;
                    }
                    
                    static void TestListFor()
                    {
                        long totalValue = 0;
                        for (int i = 0; i < g_IntList.Count; i++)
                            totalValue += i;
                    }
                    


                    Поэтому основной вывод статьи
                    Как видно, для небольших списков лучше всего использовать стандартный цикл с внешним счётчиком. По производительности ForEach проигрывает ему в разы.

                    мягко выражаясь, некорректен.

                    List.ForEach на моей машине (с) работает медленее, чем foreach, если totalValue используется после цикла (даже в виде return totalValue)

                    Микрооптимизация — [почти всегда] зло.
                      0
                      Поэтому основной вывод статьи мягко выражаясь, некорректен. Не в разы.


                      Этот вывод я делал не на основании TestListFor и TestArrayFor, а на основании написанного мной скрипта.
                        0
                        Ок. тогда статья некорректна только до примечаний переводчика.
                          0
                          Цифры в оригинальной статье сходятся с данными полученными мной с помощью собственного скрипта, в котором считаются не индексы, а элементы. Так что не знаю, в чём у вас дело.
                            +1
                            У меня как раз все нормально. на 100КК элементах, 20 итерациях, релиз, x64 с оптимизациями.
                            for — 220ms
                            List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
                            foreach — 401ms

                            Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
                              0
                              С чем вы собственно спорите?

                              for работает быстрее List.ForEach, который в свою очередь работает быстрее foreach, что показывают и ваши данные.

                              И вообще это данные для корректного или некорректного теста?
                                0
                                Это данные для корректного теста.
                                for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.

                                На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах). А вы пытаетесь использовать свои результаты как подтверждение оригинальных замеров.

                                List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.
                                  0
                                  for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.

                                  Практика подтверждает теорию, а не наоборот. На практике, же согласно оригинальной статье, for без оптимизации компилятора работает медленнее List.ForEach.

                                  На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах).

                                  Честно неуловил, в чём вы нашли отличие цикла от перебора?

                                  List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.

                                  То вы говорите, что нужно смотреть в исходники, то на фазу луны. Автор сделал предположение:
                                  Он будет вызывать callvirt IL инструкцию, но два вызова медленнее, чем один callvirt. Итак, я ожидаю, что List<T>.ForEach на самом деле будет быстрее.

                                  После чего провёл вычислительный эксперимент, при разных условиях, подтверждающий его теорию. Вы тоже провели эксперимент показывающий, хоть и не такое большое, но превосходство ForEach, но при этом утверждаете, что List.ForEach быстрее, чем foreach — очень спорное утверждение. В чём же его спорность?
                                    0
                                    Практика подтверждает теорию, а не наоборот. На практике, же согласно оригинальной статье, for без оптимизации компилятора работает медленнее List.ForEach.

                                    Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.
                                    Честно неуловил, в чём вы нашли отличие цикла от перебора?

                                    Автор вызывает цикл один раз (и один раз тратит время на создание энумератора, поэтому цена создания энумератора вообще никак не влияет на его результаты). Вы вызываете цикл 1000 раз (или больше), 1000 раз тратите время на создание энумератора, да еще и измеряете затраты на внешний цикл и switch (насколько я понял). Поэтому у вас в левой стороне графика такие сильные расхождения. Ну и опять же — JIT на вашей платформе ведет себя совсем не так, как JIT на платформе автора. Вы сравниваете разные вещи, просто графики получились похожие.
                                    В чём же его спорность

                                    Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
                                    Вот результаты с моей машины (с) на 20 попытках:
                                    ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
                                    foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
                                    Средние рядом, разброс значений гораздо больше чем разница средних.
                                    С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
                                    В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?
                                      0
                                      Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.

                                      Очень интересно, не знал.

                                      Автор вызывает цикл один раз (и один раз тратит время на создание энумератора, поэтому цена создания энумератора вообще никак не влияет на его результаты). Вы вызываете цикл 1000 раз (или больше), 1000 раз тратите время на создание энумератора, да еще и измеряете затраты на внешний цикл и switch (насколько я понял). Поэтому у вас в левой стороне графика такие сильные расхождения.

                                      Ну да, я вызываю энумератор около миллиона раз, чтобы получить усреднённое значение. В таком случае, затратами на внешний цикл и switch можно пренебречь.

                                      Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
                                      Вот результаты с моей машины (с) на 20 попытках:
                                      ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
                                      foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
                                      Средние рядом, разброс значений гораздо больше чем разница средних.
                                      С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
                                      В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?

                                      Хорошие данные, которые опять же показывают, что ForEach немного, но быстрее foreach.
                                        0
                                        >Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.

                                        Не по-этому.

                                        List внутри использует массив T[]. Для массивов в CLI есть специальные опкоды (получение длинны, получение элемента и установка элемента). Если вы делаете for по List, то элементы будут получаться через индексатор, а длина через свойство, причем через виртуальный вызов.

                                        Для массива:

                                        IL_0000: nop
                                        IL_0001: ldc.i4.0
                                        IL_0002: stloc.0
                                        IL_0003: br.s IL_0017
                                        // loop start (head: IL_0017)
                                        IL_0005: nop
                                        IL_0006: ldarg.1
                                        IL_0007: ldloc.0
                                        IL_0008: ldelem.i4
                                        IL_0009: stloc.1
                                        IL_000a: ldarg.0
                                        IL_000b: ldloc.1
                                        IL_000c: call instance void ClassLibrary2.Class1::Work(int32)
                                        IL_0011: nop
                                        IL_0012: nop
                                        IL_0013: ldloc.0
                                        IL_0014: ldc.i4.1
                                        IL_0015: add
                                        IL_0016: stloc.0

                                        IL_0017: ldloc.0
                                        IL_0018: ldarg.1
                                        IL_0019: ldlen
                                        IL_001a: conv.i4
                                        IL_001b: clt
                                        IL_001d: stloc.2
                                        IL_001e: ldloc.2
                                        IL_001f: brtrue.s IL_0005
                                        // end loop

                                        IL_0021: ret

                                        для списка:

                                        IL_0000: nop
                                        IL_0001: ldc.i4.0
                                        IL_0002: stloc.0
                                        IL_0003: br.s IL_001b
                                        // loop start (head: IL_001b)
                                        IL_0005: nop
                                        IL_0006: ldarg.1
                                        IL_0007: ldloc.0
                                        IL_0008: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1::get_Item(int32)
                                        IL_000d: stloc.1
                                        IL_000e: ldarg.0
                                        IL_000f: ldloc.1
                                        IL_0010: call instance void ClassLibrary2.Class1::Work(int32)
                                        IL_0015: nop
                                        IL_0016: nop
                                        IL_0017: ldloc.0
                                        IL_0018: ldc.i4.1
                                        IL_0019: add
                                        IL_001a: stloc.0

                                        IL_001b: ldloc.0
                                        IL_001c: ldarg.1
                                        IL_001d: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1::get_Count()
                                        IL_0022: clt
                                        IL_0024: stloc.2
                                        IL_0025: ldloc.2
                                        IL_0026: brtrue.s IL_0005
                                        // end loop

                                        IL_0028: ret
                                          +2
                                          IL код не показывает полной картины. Смотрите disassembly:
                                          код на C#:
                                          static long TestListFor()
                                          {
                                              Debugger.Launch();
                                              long totalValue = 0;
                                              for (int i = 0; i < g_IntList.Count; i++)
                                              {
                                                  totalValue += g_IntList[i];
                                              }
                                              return totalValue;
                                          }
                                          

                                          IL:
                                          .method private hidebysig static int64  TestListFor() cil managed
                                          {
                                            // Code size       47 (0x2f)
                                            .maxstack  3
                                            .locals init ([0] int64 totalValue,
                                                     [1] int32 i)
                                            IL_0000:  call       bool [mscorlib]System.Diagnostics.Debugger::Launch()
                                            IL_0005:  pop
                                            IL_0006:  ldc.i4.0
                                            IL_0007:  conv.i8
                                            IL_0008:  stloc.0
                                            IL_0009:  ldc.i4.0
                                            IL_000a:  stloc.1
                                            IL_000b:  br.s       IL_0020
                                            IL_000d:  ldloc.0
                                            IL_000e:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
                                            IL_0013:  ldloc.1
                                            IL_0014:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
                                            IL_0019:  conv.i8
                                            IL_001a:  add
                                            IL_001b:  stloc.0
                                            IL_001c:  ldloc.1
                                            IL_001d:  ldc.i4.1
                                            IL_001e:  add
                                            IL_001f:  stloc.1
                                            IL_0020:  ldloc.1
                                            IL_0021:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
                                            IL_0026:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
                                            IL_002b:  blt.s      IL_000d
                                            IL_002d:  ldloc.0
                                            IL_002e:  ret
                                          } // end of method Program::TestListFor
                                          


                                          Реально выполняемый код (кусок от зануления аккумулятора до jump на начало списка. Нет ни одного call, выполняется тривиальный цикл по элементам:
                                          ; totalValue = 0
                                          00007FFA38F30441  xor         ecx,ecx  
                                          ; for
                                          ; проверка i < list.Count и границ внутреннего массива
                                          00007FFA38F30443  mov         r9,0BF57335778h  
                                          00007FFA38F3044D  mov         r9,qword ptr [r9]  
                                          00007FFA38F30450  mov         eax,dword ptr [r9+18h]  
                                          00007FFA38F30454  cmp         edx,eax  
                                          00007FFA38F30456  jl          00007FFA38F30460  
                                          00007FFA38F30458  mov         rax,rcx  
                                          00007FFA38F3045B  jmp         00007FFA38F30486  
                                          00007FFA38F3045D  nop         dword ptr [rax]  
                                          00007FFA38F30460  cmp         edx,eax  
                                          00007FFA38F30462  jae         00007FFA38F30495  
                                          00007FFA38F30464  mov         r9,qword ptr [r9+8]  
                                          00007FFA38F30468  movsxd      r10,r8d  
                                          00007FFA38F3046B  mov         rax,qword ptr [r9+8]  
                                          00007FFA38F3046F  cmp         r10,rax  
                                          00007FFA38F30472  jae         00007FFA38F30490  
                                          ; загрузка i-го элемента в eax, в IL выглядело как callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1::get_Item(int32)
                                          00007FFA38F30474  mov         eax,dword ptr [r9+r10*4+10h]  
                                          00007FFA38F30479  movsxd      rax,eax  
                                          ; totalValue += rax
                                          00007FFA38F3047C  add         rcx,rax  
                                          00007FFA38F3047F  inc         edx  
                                          00007FFA38F30481  inc         r8d  
                                          ; jump на начало for
                                          00007FFA38F30484  jmp         00007FFA38F30443  
                                          


                                          JIT знает о стандартных генериках. Т.е. в IL callvirt есть — а при выполнении его уже нет.
                                          Для проверки лучше цеплять дебаггер уже в процессе, после того, как JIT отрабтал. Иначе получите код без JIT оптимизаций (т.е. медленный и с call).
                                            0
                                            Для сравнения, вот во что JIT превращает тот же код, если отладчик прицеплен с самого начала:
                                            00007FFA38F54014  mov         qword ptr [rsp+20h],0  
                                            00007FFA38F5401D  mov         dword ptr [rsp+28h],0  
                                            00007FFA38F54025  jmp         00007FFA38F54079  
                                            00007FFA38F54027  mov         rcx,0B42D95E728h  
                                            00007FFA38F54031  mov         rcx,qword ptr [rcx]  
                                            00007FFA38F54034  mov         rax,qword ptr [rsp+20h]  
                                            00007FFA38F54039  mov         qword ptr [rsp+30h],rax  
                                            00007FFA38F5403E  mov         qword ptr [rsp+38h],rcx  
                                            00007FFA38F54043  mov         rax,qword ptr [rsp+38h]  
                                            00007FFA38F54048  cmp         byte ptr [rax],0  
                                            00007FFA38F5404B  mov         rcx,qword ptr [rsp+38h]  
                                            00007FFA38F54050  mov         edx,dword ptr [rsp+28h]  
                                            ; вызов get_Item(int32)
                                            00007FFA38F54054  call        00007FFA9743F430  
                                            00007FFA38F54059  mov         dword ptr [rsp+40h],eax  
                                            00007FFA38F5405D  movsxd      rcx,dword ptr [rsp+40h]  
                                            00007FFA38F54062  mov         rax,qword ptr [rsp+30h]  
                                            ; totalValue += element
                                            00007FFA38F54067  add         rax,rcx  
                                            00007FFA38F5406A  mov         qword ptr [rsp+20h],rax  
                                            00007FFA38F5406F  mov         eax,dword ptr [rsp+28h]  
                                            00007FFA38F54073  inc         eax  
                                            00007FFA38F54075  mov         dword ptr [rsp+28h],eax  
                                            00007FFA38F54079  mov         rcx,0B42D95E728h  
                                            00007FFA38F54083  mov         rcx,qword ptr [rcx]  
                                            00007FFA38F54086  mov         eax,dword ptr [rsp+28h]  
                                            00007FFA38F5408A  mov         dword ptr [rsp+44h],eax  
                                            00007FFA38F5408E  mov         qword ptr [rsp+48h],rcx  
                                            00007FFA38F54093  mov         rax,qword ptr [rsp+48h]  
                                            00007FFA38F54098  cmp         byte ptr [rax],0  
                                            00007FFA38F5409B  mov         rcx,qword ptr [rsp+48h]  
                                            ; вызов Count и проверка i < Count
                                            00007FFA38F540A0  call        00007FFA9743F470  
                                            00007FFA38F540A5  mov         dword ptr [rsp+50h],eax  
                                            00007FFA38F540A9  mov         eax,dword ptr [rsp+50h]  
                                            00007FFA38F540AD  cmp         dword ptr [rsp+44h],eax  
                                            00007FFA38F540B1  jl          00007FFA38F54027  
                                            

                        +1
                        Для небольших списков нужно использовать foreach, потому что он куда как понятней, и отлавливает ошибки вроде изменения коллекции, а разница в производительности здесь мизерная.

                        Для больших списков лучше, опять же, использовать foreach, если нет каких-либо веских доводов делать что-то другое (т.е. — конкретная проблема с производительностью в данном месте, решаемая подобными микрооптимизациями).
                          0
                          Результат выполнения программы автора у меня (release-mode):
                          Initializing 100,000,000 items…

                          Testing for on int[]…
                          Executed in 0.275374 seconds.

                          Testing foreach on int[]…
                          Executed in 0.485148 seconds.

                          Testing Array.ForEach…
                          Executed in 0.528751 seconds.

                          Testing for on List…
                          Executed in 0.569238 seconds.

                          Testing foreach on List…
                          Executed in 0.548733 seconds.

                          Testing List.ForEach…
                          Executed in 0.534959 seconds.
                            0
                            Ну как уже было замечено в комментах TestListFor и TestArrayFor считают сумму индексов, видимо автор выложил не ту версию исходников.
                            Надо подредактировать эти функции
                            static void TestArrayFor()
                            {
                                long totalValue = 0;
                                for (int i = 0; i < g_IntArray.Length; i++)
                                    totalValue += g_IntArray[i];
                            }
                            
                            static void TestListFor()
                            {
                                long totalValue = 0;
                                for (int i = 0; i < g_IntList.Count; i++)
                                    totalValue += g_IntArray[i];
                            }
                            
                            0
                            Пара полезных ссылок по теме.
                            У timyrik20 был не так давно подобный пост: foreach or for that is the question.
                            В BenchmarkDotNet есть аккуратные бенчмарки над списками и массивами.
                              0
                              В общем, примерно как я и ожидал, ну не может быть List(T).ForEach быстрее
                              image

                              Причем только по 1 циклу на каждый метод (во всех тестах, что я видел и что я писал, всегда использовалось какое-то число прогонов, и брался средний результат, можно пойти дальше, и отбрасывать слишком плохие или слишком хорошие результаты, но достаточно и простого осреднения в первом приближении), нету GC.Collect — из-за чего сборка мусора может произойти прямо во время теста и повлиять на время прохождения (все-таки массивы и списки немаленькие...).

                              В общем, ИМХО на 4.0 и выше — неактуально. Цифры выше приведены для 4.5.1 release.

                              Кстати, вопрос знатокам: а почему по списку настолько дольше обход? Я думал, он на этапе обхода ведет себя как обычный массив — ведь внутри это и есть просто массив. Такой большой оверхед из-за чего? Лишь из-за проверок границ массива?
                                0
                                > Причем только по 1 циклу на каждый метод
                                > нету GC.Collect

                                Я правильно понял, что это как раз ваши циферки в консоли?
                                  0
                                  GC.Collect я вставил, но он не учитывается во времени. Вот так:
                                          private static void RunTest(string header, TestMethod testMethod)
                                          {
                                              HighResolutionTimer timer = new HighResolutionTimer();
                                              GC.Collect();
                                              GC.WaitForPendingFinalizers();
                                              GC.Collect();
                                  
                                              Console.WriteLine(header);
                                              timer.Start();
                                              testMethod();
                                              timer.Stop();
                                              Console.WriteLine("Executed in {0}.", timer);
                                              Console.WriteLine();
                                          }
                                  

                                  ну и исправил сумму не индексов, а элементов по индексам, как подсказали выше.
                                  остальное все так же.
                                0
                                -комментарий промахнулся-

                                Only users with full accounts can post comments. Log in, please.