Недавно рефаторил проект, делал универсальные методы, абстракции в аргументах вместо конкретных типов, и заметил, что производительность приложения незначительно упала. Начал копать, добрался до того, что виноват во всём цикл foreach.
Кажется, foreach оптимизирован для массива. Решил протестировать.
Вот 2 совершенно одинаковых метода, исходные данные - один и тот же массив. Разница только в сигнатуре.
class Program
{
static void Main(string[] args)
{
var result = BenchmarkRunner.Run<ForeachBenchmarks>();
Console.ReadKey();
}
}
[MemoryDiagnoser]
public class ForeachBenchmarks
{
private readonly int[] _numbers = Enumerable.Repeat(1, 1000000).ToArray();
public IEnumerable<int[]> Numbers { get { yield return _numbers; } }
[Benchmark]
[ArgumentsSource(nameof(Numbers))]
public int SumArray(int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
return sum;
}
[Benchmark]
[ArgumentsSource(nameof(Numbers))]
public int SumIEnumerable(IEnumerable<int> numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
return sum;
}
}
И правда оптимизирован:
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1081 (21H1/May2021Update)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.301
[Host] : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
| Method | numbers | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|
| SumArray | Int32[1000000] | 468.9 us | 1.84 us | 1.44 us | - | - | - | - |
| SumIEnumerable | Int32[1000000] | 5,808.0 us | 44.16 us | 39.15 us | - | - | - | 32 B |
Объясните пожалуйста, почему foreach в 10 раз медленнее, если использовать интерфейс IEnumerable<T> для массива, чем если использовать T[]?
UPD: Провел тесты для List<int> и для ReadOnlySpan<int>. Для списка foreach такой же по производительности, как для IEnumerable<T>, а для спана такой же как для массива.
ICollectionсразу берётся значение свойства. Почему подобное не сделали для форыча? Открыли бы Мелкомягкие доступ к изменению компилятора, энтузиасты давно бы сделали такие оптимизации. – Alexander Petrov Jul 18 '21 at 15:26IList, потому что внутри может быть что угодно. '@aepot, при явном указанииIListоптимизации все равно нет. Контрактforeachнельзя нарушать, я могу написать реализациюIList, которая будет бросать исключение при обращении к индексатору, но при этом нормально работать сGetEnumerator().MoveNext()иforeachдолжен работать! – Андрей NOP Jul 20 '21 at 11:09