Поиск самого быстрого способа перебора массивов при доступе к их значению в .NET с помощью BenchmarkDotNet проверяет производительность в масштабе.

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

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

Итерация и доступ к массивам

Сегодня я решил рассмотреть множество различных способов перебора массива. В этом нет ничего сложного, но, учитывая, что программисты постоянно используют в C# и списки, и массивы, обращение к ним тоже имеет смысл.

Ключом к результату является присвоение элемента из массива переменной. Это гарантирует, что значение считывается, а повторение без доступа к элементу может привести к искаженным результатам.

Ниже приведены методы, которые используются и тестируются через BenchmarkDotNet.

Теория

Учитывая сходство с предыдущим тестом, я полагаю, что методы Span будут здесь нашим явным победителем. Не имея методов LINQ, я выберу метод GetEnumerator, который будет наименее эффективным из всех. Я ожидаю, что петля for будет довольно близко к Span, а foreach будет немного дальше.

Настраивать

Использование BenchmarkDotNet для создания консольного приложения делает выполнение этого сравнительного теста очень простым. Чтобы обеспечить охват старых систем, эти тесты будут выполняться как в .NET Framework 4.8, так и в .NET 6.0, поскольку это текущие версии .NET с долгосрочной поддержкой. Один для старого мира и один для нового мира.

Предпосылка проста. Создайте Array строк глубиной 100 и 10 000. Затем запустите тест, чтобы перебрать каждый элемент, и повторяйте это до тех пор, пока BenchmarkDotNet не предоставит нам результат. Это должно дать нам разумное предположение о самом быстром способе сделать это в будущем.

Методы

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

Полученные результаты

К счастью для нас, BenchmarkDotNet включает столбец Рейтинг, чтобы было ясно, какой из тестов является победителем. Когда вы читаете эти результаты, вы можете использовать столбец Среднее, чтобы сравнить разницу во времени выполнения между каждым тестом. Для некоторых из них есть явный победитель, а для других разница незначительна.

.NET Framework 4.8 — итерация и доступ

| Rank | Method        | N     |           Mean |
| ---: | ------------- | ----- | -------------: |
|    1 | ForEachLoop   | 100   |       305.9 ns |
|    2 | ForLoop       | 100   |       343.9 ns |
|    3 | ArrayForEach  | 100   |       451.1 ns |
|    4 | GetEnumerator | 100   |    10,561.3 ns |
|    5 | ForEachLoop   | 10000 |    30,064.2 ns |
|    6 | ForLoop       | 10000 |    33,541.9 ns |
|    7 | ArrayForEach  | 10000 |    43,825.3 ns |
|    8 | GetEnumerator | 10000 | 1,055,741.5 ns |

.NET 6.0 — Итерация и доступ

| Rank | Method        | N     |         Mean |
| ---: | ------------- | ----- | -----------: |
|    1 | SpanForEach   | 100   |     141.9 ns |
|    2 | ForLoop       | 100   |     173.6 ns |
|    2 | ForEachLoop   | 100   |     175.2 ns |
|    2 | SpanFor       | 100   |     175.5 ns |
|    3 | ArrayForEach  | 100   |     370.7 ns |
|    4 | GetEnumerator | 100   |   6,190.5 ns |
|    5 | SpanForEach   | 10000 |  12,333.7 ns |
|    6 | ForLoop       | 10000 |  16,181.6 ns |
|    6 | SpanFor       | 10000 |  16,187.0 ns |
|    6 | ForEachLoop   | 10000 |  16,251.8 ns |
|    7 | ArrayForEach  | 10000 |  36,155.1 ns |
|    8 | GetEnumerator | 10000 | 611,853.5 ns |

Мысли

.NET Framework 4.8

Что ж, если честно, это довольно неожиданно, по результатам теста foreach явный победитель. Учитывая ужасную производительность на List, я действительно не ожидал этого. Он быстрее перебирает больший и меньший массив, чем цикл for. У массивов появился новый чемпион. Как и ожидалось, GetEnumerator является худшим исполнителем в абсолютном выражении. Однако результат здесь ясен: пора рефакторить код, если вы Array.Foreach предпочитаете foreach. Вы можете взять его или оставить с for петлями, так как разница невелика.

.NET 6.0

Это знакомая история для .NET 6.0: Span foreach методов забирают приз. За ним следуют for, foreach и Span for, что наводит меня на мысль, что здесь не стоит серьезно заниматься рефакторингом. Есть только незначительные улучшения, но любой новый код должен использовать Span в сочетании с foreach. Array.ForEach здесь сильно отстает и полностью заслуживает рефакторинга. GetEnumerator было достаточно записано, это просто не стоит вашего времени.

ForEach FTW

Хотя в некоторых областях жизнь не меняется, цикл foreach является явным победителем, и его следует реализовать в качестве предпочтительного итератора для приложений .NET 6.0 и .NET Framework 4.8. Просто не забудьте с .NET 6.0 сделать его также Span.

Код

Весь этот код находится в открытом доступе. Вы можете найти мою работу по бенчмаркингу на GitHub. Результаты являются выходными данными GitHub Action для запуска эталонных тестов, вы сможете найти подробные выходные данные в этом репозитории.

Support
If you like this, or want to checkout my other work, please connect with me on LinkedIn, Twitter or GitHub.