Комментарии 39
Неужели есть в мире приложения в которых разница в скорости между for и foreach будет иметь хоть сколь-нибудь важное значение?
А уж сравнивать задержка на 1, 10 или 100 проходах вообще странно. Естественно время, затраченное на инициализацию будет значительно влиять.
Ну и конечно habrahabr.ru/post/192130/
А уж сравнивать задержка на 1, 10 или 100 проходах вообще странно. Естественно время, затраченное на инициализацию будет значительно влиять.
Ну и конечно habrahabr.ru/post/192130/
+4
Интересная статья, жаль не попалась мне раньше. Что впрочем не отменяет ценность этой статьи. В вашей ссылке написано, что List.ForEach показал худший результат. Здесь же видно, что для длинных списков его производительность такая же как у for-loop. Плюс её можно записывать более элегантно и использовать делегаты.
0
У foreach есть побочный эффект в виде создания объекта итератора, что на мобильных платформах может аукнуться при интенсивном использовании циклов.
0
Для List и прочих встроенных типов коллекций, энумераторы обхявлены как value types, поэтому их создание не несет существенных накладных расходов. Плюс, они создаются по штуке на цикл (а не итерацию).
0
Вы бы сравнили с 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
}
}
А всё из-за обезьян, которые не смогли осилить замыкание.
-1
Ну это всё-таки перевод, а Я работаю на C# 3.2.4.
0
Почитал про foreach C# 5, и что-то мне кажется, что это фича, а не баг.
Поэтому я ой не торопился бы про обезьян.
Поэтому я ой не торопился бы про обезьян.
0
Ткните пальцем туда, где я написал что это баг. Эрик Липперт в своём блоге как раз и писал, что они это сделали из-за «баг репортов» тех самых обезьян, которые не понимают что такое замыкание.
0
Ясно. Лично я подумал, что про обезьян — это вы о разработчиках компилятора :)
0
Все равно ваша логика непонятна. То, как сделано в C# 5 — это как раз так, как оно должно было быть с самого начала, при наличии замыканий. Но замыканий в С# 1.0 не было, поэтому сделали как проще — а когда их добавили в 2.0, то это решение пересмотрено не было.
«Обезьяны» тут совершенно ни при чем, т.к. нет каких-либо причин интуитивно предпочитать первое раскрытие второму — скорее наоборот, потому что нет никакого смысла в том, чтобы держать общую переменную для всего цикла — ну нет такого сценария, где хочется её захватить в замыкании, и увидеть изменение на следующем цикле.
К производительности это все не имеет никакого отношения, т.к. переменная не «выделяется» на каждой итерации цикла.
«Обезьяны» тут совершенно ни при чем, т.к. нет каких-либо причин интуитивно предпочитать первое раскрытие второму — скорее наоборот, потому что нет никакого смысла в том, чтобы держать общую переменную для всего цикла — ну нет такого сценария, где хочется её захватить в замыкании, и увидеть изменение на следующем цикле.
К производительности это все не имеет никакого отношения, т.к. переменная не «выделяется» на каждой итерации цикла.
+2
Было: поведение контринтуитивное и совершенно бесполезное для программистов. Хак компилятора наружу, протекшая абстракция.
Стало: поведение предполагаемое любым разумным человеком не читавшим описание foreach и удобное для программистов.
А вот почему вдруг новое поведение более ресурсоемкое… тут знаете надо оптимизатор точить, а не программистов обезьянами называть.
Стало: поведение предполагаемое любым разумным человеком не читавшим описание foreach и удобное для программистов.
А вот почему вдруг новое поведение более ресурсоемкое… тут знаете надо оптимизатор точить, а не программистов обезьянами называть.
0
Во-первых, это breaking changes, во вторых — раньше оно работало так же как и for, в котором замыкание всё ещё есть.
0
Во-первых, это breaking changes
xkcd.com/1172
раньше оно работало так же как и for
А сейчас оно работает так же, как ForEach, что гораздо логичнее.
Может кто-то и использует регулярно for в своей работе, но я, к счастью, с такой необходимостью давно не встречался.
0
>>А сейчас оно работает так же, как ForEach, что гораздо логичнее.
Чтоб вы знали, List::ForEach сами MS считают злом (и намеренно не делали LINQ расширение) и успешно выпилили из Windows 8 этот метод из листа. Если хотите, могу скинуть сцылку на соответсвующую блогозапись Эрика Липперта.
Чтоб вы знали, List::ForEach сами MS считают злом (и намеренно не делали LINQ расширение) и успешно выпилили из Windows 8 этот метод из листа. Если хотите, могу скинуть сцылку на соответсвующую блогозапись Эрика Липперта.
0
Хорошо, вы меня вынудили таки почитать его статьи по этому поводу.
Притензии к ForEach у него исключительно семантические: LINQ не для сайдэффектов. Не могу с ним не согласиться, особенно в синтаксисе C#.
Про foreach же он высказывается весьма однозначно:
Тут же указано (жирный шрифт) существенное отличие foreach от for.
Да, он приводит несколько причин для существования текущего поведения, но я готов с ними поспорить. Моим тезисом было бы ассоциированные foreach с ForEach, в не с for. Так, сделано, например, в scala.
Ну и да, immutable переменная при замыкании должна иметь поведение копирования значения.
Притензии к 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 переменная при замыкании должна иметь поведение копирования значения.
0
Нет, не работало.
-1
> ForEach-петлям
> петлям
Циклы же…
> петлям
Циклы же…
+5
тесты TestListFor и TestArrayFor считают сумму индексов. Остальные тесты — считают сумму элементов списка/массива.
Поэтому основной вывод статьи
мягко выражаясь, некорректен.
List.ForEach на моей машине (с) работает медленее, чем foreach, если totalValue используется после цикла (даже в виде return totalValue)
Микрооптимизация — [почти всегда] зло.
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)
Микрооптимизация — [почти всегда] зло.
+1
Поэтому основной вывод статьи мягко выражаясь, некорректен. Не в разы.
Этот вывод я делал не на основании TestListFor и TestArrayFor, а на основании написанного мной скрипта.
0
Ок. тогда статья некорректна только до примечаний переводчика.
0
Цифры в оригинальной статье сходятся с данными полученными мной с помощью собственного скрипта, в котором считаются не индексы, а элементы. Так что не знаю, в чём у вас дело.
0
У меня как раз все нормально. на 100КК элементах, 20 итерациях, релиз, x64 с оптимизациями.
for — 220ms
List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
foreach — 401ms
Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
for — 220ms
List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
foreach — 401ms
Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
+1
С чем вы собственно спорите?
for работает быстрее List.ForEach, который в свою очередь работает быстрее foreach, что показывают и ваши данные.
И вообще это данные для корректного или некорректного теста?
for работает быстрее List.ForEach, который в свою очередь работает быстрее foreach, что показывают и ваши данные.
И вообще это данные для корректного или некорректного теста?
0
Это данные для корректного теста.
for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.
На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах). А вы пытаетесь использовать свои результаты как подтверждение оригинальных замеров.
List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.
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
Не по-этому.
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
0
IL код не показывает полной картины. Смотрите disassembly:
код на C#:
IL:
Реально выполняемый код (кусок от зануления аккумулятора до jump на начало списка. Нет ни одного call, выполняется тривиальный цикл по элементам:
JIT знает о стандартных генериках. Т.е. в IL callvirt есть — а при выполнении его уже нет.
Для проверки лучше цеплять дебаггер уже в процессе, после того, как JIT отрабтал. Иначе получите код без JIT оптимизаций (т.е. медленный и с call).
код на 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).
+2
Для сравнения, вот во что 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
0
Для небольших списков нужно использовать foreach, потому что он куда как понятней, и отлавливает ошибки вроде изменения коллекции, а разница в производительности здесь мизерная.
Для больших списков лучше, опять же, использовать foreach, если нет каких-либо веских доводов делать что-то другое (т.е. — конкретная проблема с производительностью в данном месте, решаемая подобными микрооптимизациями).
Для больших списков лучше, опять же, использовать foreach, если нет каких-либо веских доводов делать что-то другое (т.е. — конкретная проблема с производительностью в данном месте, решаемая подобными микрооптимизациями).
+1
Результат выполнения программы автора у меня (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 есть аккуратные бенчмарки над списками и массивами.
У timyrik20 был не так давно подобный пост: foreach or for that is the question.
В BenchmarkDotNet есть аккуратные бенчмарки над списками и массивами.
0
В общем, примерно как я и ожидал, ну не может быть List(T).ForEach быстрее
Причем только по 1 циклу на каждый метод (во всех тестах, что я видел и что я писал, всегда использовалось какое-то число прогонов, и брался средний результат, можно пойти дальше, и отбрасывать слишком плохие или слишком хорошие результаты, но достаточно и простого осреднения в первом приближении), нету GC.Collect — из-за чего сборка мусора может произойти прямо во время теста и повлиять на время прохождения (все-таки массивы и списки немаленькие...).
В общем, ИМХО на 4.0 и выше — неактуально. Цифры выше приведены для 4.5.1 release.
Кстати, вопрос знатокам: а почему по списку настолько дольше обход? Я думал, он на этапе обхода ведет себя как обычный массив — ведь внутри это и есть просто массив. Такой большой оверхед из-за чего? Лишь из-за проверок границ массива?
Причем только по 1 циклу на каждый метод (во всех тестах, что я видел и что я писал, всегда использовалось какое-то число прогонов, и брался средний результат, можно пойти дальше, и отбрасывать слишком плохие или слишком хорошие результаты, но достаточно и простого осреднения в первом приближении), нету GC.Collect — из-за чего сборка мусора может произойти прямо во время теста и повлиять на время прохождения (все-таки массивы и списки немаленькие...).
В общем, ИМХО на 4.0 и выше — неактуально. Цифры выше приведены для 4.5.1 release.
Кстати, вопрос знатокам: а почему по списку настолько дольше обход? Я думал, он на этапе обхода ведет себя как обычный массив — ведь внутри это и есть просто массив. Такой большой оверхед из-за чего? Лишь из-за проверок границ массива?
0
> Причем только по 1 циклу на каждый метод
> нету GC.Collect
Я правильно понял, что это как раз ваши циферки в консоли?
> нету 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
-комментарий промахнулся-
0
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Что быстрее? foreach vs. List.ForEach vs. for-loop