Есть ли проблема с производительностью list.ForEach() в .NET 7?

После переключения личного решения с .NET 6 на .NET 7 время чтения большого объема данных сократилось с 18 с до 4 мин 30 с (приблизительно).

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

private void SpeedTest()
{
    int nbdata = 6000000;
    List<int> list = new(nbdata);
    var rnd = RandomNumberGenerator.Create();
    Random rand = new(12345);
    for (int i = 0; i < nbdata; i++)
    {
        var rnddata = new byte[sizeof(int)];
        rnd.GetBytes(rnddata);
        list.Add(BitConverter.ToInt32(rnddata));
    }
    int[] arr = list.ToArray();

    //Begin test
    int chk = 0;
    Stopwatch watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        int len = list.Count;
        for (int i = 0; i < len; i++)
        {
            chk += list[i];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("List/for Count out: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        for (int i = 0; i < list.Count; i++)
        {
            chk += list[i];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("List/for Count in: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        int len = arr.Length;
        for (int i = 0; i < len; i++)
        {
            chk += arr[i];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("Array/for Count out: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        for (int i = 0; i < arr.Length; i++)
        {
            chk += arr[i];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("Array/for Count in: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        int k = list.Count;
        for (int j = 0; j < k; j++)
        {
            chk += list[j];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        foreach (int i in list)
        {
            chk += i;
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        list.ForEach(i => chk += i);
    }
    watch.Stop();
    SpeedText.Text += string.Format("List/foreach function: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        int k = arr.Length;
        for (int j = 0; j < k; j++)
        {
            chk += arr[j];
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;

    chk = 0;
    watch = Stopwatch.StartNew();
    for (int rpt = 0; rpt < 100; rpt++)
    {
        foreach (int i in arr)
        {
            chk += i;
        }
    }
    watch.Stop();
    SpeedText.Text += string.Format("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk) + Environment.NewLine;
}

Результат .NET 6:

List/for Count out: 1442ms (398007896)
List/for Count in: 1446ms (398007896)
Array/for Count out: 1256ms (398007896)
Array/for Count in: 1254ms (398007896)
List/for: 1435ms (398007896)
List/foreach: 1258ms (398007896)
List/foreach function: 1452ms (398007896) <=
Array/for: 1255ms (398007896)
Array/foreach: 1254ms (398007896)

Результат .NET 7:

List/for Count out: 1483ms (272044760)
List/for Count in: 1489ms (272044760)
Array/for Count out: 1255ms (272044760)
Array/for Count in: 1263ms (272044760)
List/for: 1482ms (272044760)
List/foreach: 1873ms (272044760)
List/foreach function: 7997ms (272044760) <=
Array/for: 1254ms (272044760)
Array/foreach: 1255ms (272044760)

Код этой проблемы:

list.ForEach(i => chk += i);

Эта проблема внутри .NET 7?

Есть ли у меня надежда найти решение без изменения всех вызовов этой функции?

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

Что вы порекомендуете?

Спасибо.

Обновлено:

Я использовал ForEach несколько раз для чтения кода. Изначально в .NET 6 потеря времени была приемлемой. Я использовал Tuple с данными, прочитанными в больших файлах.

Пример:

listValue.ForEach(x => process((new col(x.name, position++, startId++, x.refState, x.refPosition, x.refTable, x.withoutRef, x.deleted, x.resetData), option)));
foreach((string name, uint refState, uint refPosition, uint refTable, bool withoutRef, bool deleted, bool resetData)x in listValue)
{
    process((new col(x.name, position++, startId++, x.refState, x.refPosition, x.refTable, x.withoutRef, x.deleted, x.resetData), option))
};

Моя программа далека от завершения, и я использую общедоступные файлы данных для ее тестирования:

  • xlsx с 1 000 000 строк по 14 столбцов.
  • csv-файл с 10 000 000 строк по 14 столбцов.

Я внес некоторые изменения в свой код между переключением на .NET6 и .NET 7 и увидел, что время резко увеличилось в моем первом тесте в .NET 7. Поэтому я вернулся к своему исходному коду теста, чтобы посмотреть, есть ли какие-либо изменения, прежде чем пересматривать весь код.

Я думаю, что время обработки и код этого теста подходят для принятия решения в моем случае. Я просто хочу посмотреть, как долго пользователю придется ждать. Так что я ставлю себя в тот же случай, что и пользователь. Бенчмарк со списком из 5000 элементов не актуален. Я работаю с большим списком, и этот размер может повлиять на производительность.

Этот тест является базовым и показывает большую разницу между .NET 6 и .NET 7 с одинаковым кодом. Производительность массивов по сравнению со списками

Здесь вопрос не в том, как производится измерение, а в результате. Не используется ни одна библиотека, которая могла бы иметь разные версии и влиять на результат.

Тестирую на Windows 10 с Ryzen 1700 и RAM 16Gb.

РЕДАКТИРОВАТЬ2:

Проект для тестирования: https://github.com/gandf/TestPerfForEach

Очистите и сгенерируйте проект и запустите вне Visual Studio.

Результат .NET 6:

Test with 6000000 NbData
List/foreach: 1254ms (2107749308)
List/foreach function: 1295ms (2107749308)
Test with 6000000 NbData
List/foreach: 1259ms (1107007452)
List/foreach function: 1255ms (1107007452)
Test with 6000000 NbData
List/foreach: 1253ms (745733412)
List/foreach function: 1256ms (745733412)
Test with 6000000 NbData
List/foreach: 1253ms (-280872836)
List/foreach function: 1259ms (-280872836)

Результат .NET 7:

Test with 6000000 NbData
List/foreach: 1866ms (-998431744)
List/foreach function: 8347ms (-998431744)
Test with 6000000 NbData
List/foreach: 1753ms (715062008)
List/foreach function: 1368ms (715062008)
Test with 6000000 NbData
List/foreach: 1754ms (667927108)
List/foreach function: 1335ms (667927108)
Test with 6000000 NbData
List/foreach: 1749ms (310491380)
List/foreach function: 1366ms (310491380)

Одно и то же условие и тесты выполняются несколько раз:

  1. .NET 6 быстрее.
  2. Проблема с list.ForEach только при первом запуске. После быстрее, чем foreach.

...почему вы используете .ForEach вместо foreach? Использование .ForEach всегда будет медленнее, потому что вы создаете замыкание, а это означает, что chk должно быть выделено в куче (что плохо), и вы, вероятно, потеряете временную локальность (что тоже плохо).

Dai 10.12.2022 01:47

К вашему сведению, есть лучший способ измерить такую ​​​​производительность: github.com/dotnet/BenchmarkDotNet

Rand Random 10.12.2022 01:48

@Dai Я использовал ForEach несколько раз из-за моего предыдущего теста на .NET 6 (приемлемая потеря производительности), потому что он упростил чтение кода. Здесь эталон используется для принятия решения: какой код использовать в соответствии с потребностями и производительностью. Работа со списком Tuple. Я дополню свой вопрос.

Florent H. 10.12.2022 09:49
foreach((string name, uint refState, uint refPosition, uint refTable, bool withoutRef, bool deleted, bool resetData)x in listValue) -- Почему бы не просто foreach(var x in listValue) вместо этого?
Theodor Zoulias 10.12.2022 10:08

Мне не нравится эта формулировка для обслуживания.

Florent H. 10.12.2022 10:28

Вы имеете в виду, что вам не нравится var, потому что тип неявный? Если да, то почему бы вам явно не указать тип аргумента и в лямбда-выражении ForEach? listValue.ForEach((LooongType x) =>

Theodor Zoulias 10.12.2022 10:55

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

Filip Cordas 10.12.2022 11:01

@HansPassant Хорошее видео. Это объясняет, почему в .NET 7 результаты хуже. Но это не объясняет, почему первый запуск ForEach медленнее x6, а после — быстрее, чем foreach.

Florent H. 10.12.2022 12:15
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
165
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Используя BenchmarkDotNet, я попытался воссоздать ваш сценарий, а затем запустил его как для .NET6, так и для .NET7.

Я использовал меньшие числа, потому что инструмент бенчмаркинга может занять минуту.

Вот код, который я использовал:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using System.Security.Cryptography;

namespace Experiments
{
    [MemoryDiagnoser]
    [Orderer(SummaryOrderPolicy.FastestToSlowest)]
    [RankColumn]
    //[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net70)]
    public class ForEachBenchmark
    {
        [Params(100, 1_000)]
        public int N;

        [Params(5_000)]
        public int NbData;

        private int[] arr = Array.Empty<int>();
        private List<int> list = new List<int>();

        [GlobalSetup]
        public void Setup()
        {
            arr = new int[NbData];

            var rnd = RandomNumberGenerator.Create();

            for (int i = 0; i < NbData; i++)
            {
                var rnddata = new byte[sizeof(int)];
                rnd.GetBytes(rnddata);
                arr[i] = BitConverter.ToInt32(rnddata);
            }

            list = new List<int>(arr[..N]);
        }

        [Benchmark]
        public void ForLoop()
        {
            int chk = 0;
            for (int rpt = 0; rpt < N; rpt++)
            {
                chk += arr[rpt];
            }
        }

        [Benchmark]
        public void ForEachLoop()
        {
            int chk = 0;
            foreach (var rpt in arr[..N])
            {
                chk += rpt;
            }
        }

        [Benchmark]
        public void ListForEachLoop()
        {
            int chk = 0;
            list.ForEach(l => chk += l);
        }
    }
}

Вот Program.cs в моем консольном приложении:

using BenchmarkDotNet.Running;

BenchmarkRunner.Run<ForEachBenchmark>();

Вот мои результаты:

.NET 6

Метод Н NbData Иметь в виду Ошибка стандартное отклонение Классифицировать Gen0 Выделено ForLoop 100 5000 57,02 нс 0,583 нс 0,517 нс 1 - - ForEachLoop 100 5000 118,96 нс 2,404 нс 3,290 нс 2 0,1013 424 Б ListForEachLoop 100 5000 275,77 нс 5,468 нс 7.300 нс 3 0,0210 88 Б ForLoop 1000 5000 611,56 нс 9,434 нс 9,266 нс 4 - - ForEachLoop 1000 5000 1235,28 нс 30,499 нс 88,968 нс 5 0,9613 4024 Б ListForEachLoop 1000 5000 2478,17 нс 88,920 нс 249,342 нс 6 0,0191 88 Б

.NET 7

Метод Н NbData Иметь в виду Ошибка стандартное отклонение медиана Классифицировать Gen0 Выделено ForLoop 100 5000 55,41 нс 0,907 нс 1,080 нс 55,22 нс 1 - - ForEachLoop 100 5000 90,06 нс 2,250 нс 6,455 нс 86,91 нс 2 0,1013 424 Б ListForEachLoop 100 5000 310,84 нс 6,278 нс 15,399 нс 305,42 нс 3 0,0210 88 Б ForLoop 1000 5000 510,95 нс 10,273 нс 17,720 нс 511,14 нс 4 - - ForEachLoop 1000 5000 792,89 нс 27,420 нс 80,849 нс 789,39 нс 5 0,9613 4024 Б ListForEachLoop 1000 5000 2527,76 нс 58,979 нс 168,271 нс 2498,65 нс 6 0,0191 88 Б

На ваш взгляд, список ForEach немного замедлился между двумя версиями. Эти числа указаны в наносекундах, поэтому изменение довольно небольшое (~ 50 нс). Все остальные числа, кажется, улучшились между версиями. Распределение памяти стабильно.

ОП запросил List<int>. Почему вы вместо этого используете массив?

Theodor Zoulias 10.12.2022 05:50

Вы должны добавить библиотеки и протестировать на небольших данных. List<5000> не показывать такие же результаты, как List<6000000>.

Florent H. 10.12.2022 10:33

@ФлорентХ. Если вы считаете, что размер массива является проблемой, просто измените переменную на 10000000 и запустите ее. Сортировка массива по списку под капотом и по размеру может привести к тому, что он будет выделен в кучу больших объектов, но список ForEach представляет собой цикл for, который выполняет некоторые дополнительные проверки, поэтому единственными накладными расходами является вызов делегата, а не часть цикла.

Filip Cordas 10.12.2022 11:28

@ФлорентХ. Вы можете изменить размер теста, используя предоставленные поля. Я думаю, возможно, моя точка зрения была упущена вами и некоторыми комментаторами. Я опубликовал свой ответ не для того, чтобы продемонстрировать, что вы можете создать легко читаемое сравнение конкурирующих идей между яблоками, используя BenchmarkDotNet (без принадлежности), а скорее для того, чтобы предоставить больше доказательств того, что ваше утверждение было правильным.

Vic F 10.12.2022 14:31

@VicF В моем вопросе я использую Array только для ссылки, отличной от List. Этот вопрос не «Список против массива». Я управляю большим количеством данных, но я не могу сейчас прочитать все данные, сколько я должен получить. Мне кажется, что массивы — это непрерывные наборы памяти. Это может вызвать проблемы с большими наборами данных. Думаю со List у меня такой проблемы нет но данные могут быть разбросаны по ОЗУ.

Florent H. 10.12.2022 15:01

@VicF Я проверил ваш метод и получил те же результаты, что и вы. .NET 6 кажется быстрее в этом тесте даже при использовании списков и даже при использовании большего набора данных. Но на практике происходит не так. Когда пользователь запускает процесс, используемый цикл запускается только один раз. Ваш тест показывает только средние значения. На практике используется 1-й запуск. Я отредактировал вопрос (посмотрите EDIT2), который показывает это поведение.

Florent H. 10.12.2022 15:06

@ФлорентХ. Хорошо. Вы можете изменить его, чтобы он запускался только один раз на каждой итерации теста. Вы можете рандомизировать тест любым удобным для вас способом. Я никогда не думал, что вопрос касается списка и массива. Речь идет о производительности между двумя платформами, и, кроме List.ForEach (в этом тесте), производительность, похоже, улучшилась с .NET7. Если вам не нравится, как я настроил тест, вы можете изменить его, но каким бы ни оказался ваш тест, использование такого инструмента, как BenchmarkDotNet (или других), даст вам справедливое сравнение, которое вам нужно. принять лучшее решение.

Vic F 10.12.2022 15:58
Ответ принят как подходящий

Я нашел источник этой проблемы. Год назад я вижу это: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

Я оставил в ПУТИ 2 варианта:

  • DOTNET_ReadyToRun 0
  • DOTNET_TieredPGO 1

С этими параметрами я заметил очень небольшое ухудшение первого вызова в .NET 6 с улучшением других вызовов. Поэтому я сохранил его, потому что влияние было незначительным.

Так что есть случай, когда первый вызов занимает в 6 раз больше времени в .NET7 с этими параметрами.

Я просто удалил их. Результаты после перезагрузки:

.NET 6

Test with 6000000 NbData
List/foreach: 1263ms (-1425648688)
List/foreach function: 1312ms (-1425648688)
Test with 6000000 NbData
List/foreach: 1253ms (-1169873892)
List/foreach function: 1256ms (-1169873892)
Test with 6000000 NbData
List/foreach: 1257ms (1528933740)
List/foreach function: 1256ms (1528933740)
Test with 6000000 NbData
List/foreach: 1254ms (-1327641484)
List/foreach function: 1254ms (-1327641484)

.NET 7

Test with 6000000 NbData
List/foreach: 1470ms (991593448)
List/foreach function: 1411ms (991593448)
Test with 6000000 NbData
List/foreach: 1465ms (751941656)
List/foreach function: 1434ms (751941656)
Test with 6000000 NbData
List/foreach: 1470ms (-17227852)
List/foreach function: 1435ms (-17227852)
Test with 6000000 NbData
List/foreach: 1469ms (1422420324)
List/foreach function: 1437ms (1422420324)

Это фиксированная.

Другие вопросы по теме