После переключения личного решения с .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))
};
Моя программа далека от завершения, и я использую общедоступные файлы данных для ее тестирования:
Я внес некоторые изменения в свой код между переключением на .NET6 и .NET 7 и увидел, что время резко увеличилось в моем первом тесте в .NET 7. Поэтому я вернулся к своему исходному коду теста, чтобы посмотреть, есть ли какие-либо изменения, прежде чем пересматривать весь код.
Я думаю, что время обработки и код этого теста подходят для принятия решения в моем случае. Я просто хочу посмотреть, как долго пользователю придется ждать. Так что я ставлю себя в тот же случай, что и пользователь. Бенчмарк со списком из 5000 элементов не актуален. Я работаю с большим списком, и этот размер может повлиять на производительность.
Этот тест является базовым и показывает большую разницу между .NET 6 и .NET 7 с одинаковым кодом. Производительность массивов по сравнению со списками
Здесь вопрос не в том, как производится измерение, а в результате. Не используется ни одна библиотека, которая могла бы иметь разные версии и влиять на результат.
Тестирую на Windows 10 с Ryzen 1700 и RAM 16Gb.
Проект для тестирования: 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)
Одно и то же условие и тесты выполняются несколько раз:
list.ForEach только при первом запуске. После быстрее, чем foreach.К вашему сведению, есть лучший способ измерить такую производительность: github.com/dotnet/BenchmarkDotNet
@Dai Я использовал ForEach несколько раз из-за моего предыдущего теста на .NET 6 (приемлемая потеря производительности), потому что он упростил чтение кода. Здесь эталон используется для принятия решения: какой код использовать в соответствии с потребностями и производительностью. Работа со списком Tuple. Я дополню свой вопрос.
foreach((string name, uint refState, uint refPosition, uint refTable, bool withoutRef, bool deleted, bool resetData)x in listValue) -- Почему бы не просто foreach(var x in listValue) вместо этого?
Мне не нравится эта формулировка для обслуживания.
Вы имеете в виду, что вам не нравится var, потому что тип неявный? Если да, то почему бы вам явно не указать тип аргумента и в лямбда-выражении ForEach? listValue.ForEach((LooongType x) =>
Похоже, ваш код в целом плохой. И да, среда тестирования может давать разные результаты в зависимости от многих переменных, поэтому ваш тестовый код тоже бесполезен. даже не упомянул, как компилируется код. Пожалуйста, не делайте поспешных выводов и запускайте правильный профилировщик предпочтений для своего кода.
@HansPassant Хорошее видео. Это объясняет, почему в .NET 7 результаты хуже. Но это не объясняет, почему первый запуск ForEach медленнее x6, а после — быстрее, чем foreach.





Используя 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
.NET 7
На ваш взгляд, список ForEach немного замедлился между двумя версиями.
Эти числа указаны в наносекундах, поэтому изменение довольно небольшое (~ 50 нс).
Все остальные числа, кажется, улучшились между версиями. Распределение памяти стабильно.
ОП запросил List<int>. Почему вы вместо этого используете массив?
Вы должны добавить библиотеки и протестировать на небольших данных. List<5000> не показывать такие же результаты, как List<6000000>.
@ФлорентХ. Если вы считаете, что размер массива является проблемой, просто измените переменную на 10000000 и запустите ее. Сортировка массива по списку под капотом и по размеру может привести к тому, что он будет выделен в кучу больших объектов, но список ForEach представляет собой цикл for, который выполняет некоторые дополнительные проверки, поэтому единственными накладными расходами является вызов делегата, а не часть цикла.
@ФлорентХ. Вы можете изменить размер теста, используя предоставленные поля. Я думаю, возможно, моя точка зрения была упущена вами и некоторыми комментаторами. Я опубликовал свой ответ не для того, чтобы продемонстрировать, что вы можете создать легко читаемое сравнение конкурирующих идей между яблоками, используя BenchmarkDotNet (без принадлежности), а скорее для того, чтобы предоставить больше доказательств того, что ваше утверждение было правильным.
@VicF В моем вопросе я использую Array только для ссылки, отличной от List. Этот вопрос не «Список против массива». Я управляю большим количеством данных, но я не могу сейчас прочитать все данные, сколько я должен получить. Мне кажется, что массивы — это непрерывные наборы памяти. Это может вызвать проблемы с большими наборами данных. Думаю со List у меня такой проблемы нет но данные могут быть разбросаны по ОЗУ.
@VicF Я проверил ваш метод и получил те же результаты, что и вы. .NET 6 кажется быстрее в этом тесте даже при использовании списков и даже при использовании большего набора данных. Но на практике происходит не так. Когда пользователь запускает процесс, используемый цикл запускается только один раз. Ваш тест показывает только средние значения. На практике используется 1-й запуск. Я отредактировал вопрос (посмотрите EDIT2), который показывает это поведение.
@ФлорентХ. Хорошо. Вы можете изменить его, чтобы он запускался только один раз на каждой итерации теста. Вы можете рандомизировать тест любым удобным для вас способом. Я никогда не думал, что вопрос касается списка и массива. Речь идет о производительности между двумя платформами, и, кроме List.ForEach (в этом тесте), производительность, похоже, улучшилась с .NET7. Если вам не нравится, как я настроил тест, вы можете изменить его, но каким бы ни оказался ваш тест, использование такого инструмента, как BenchmarkDotNet (или других), даст вам справедливое сравнение, которое вам нужно. принять лучшее решение.
Я нашел источник этой проблемы. Год назад я вижу это: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/
Я оставил в ПУТИ 2 варианта:
С этими параметрами я заметил очень небольшое ухудшение первого вызова в .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)
Это фиксированная.
...почему вы используете
.ForEachвместоforeach? Использование.ForEachвсегда будет медленнее, потому что вы создаете замыкание, а это означает, чтоchkдолжно быть выделено в куче (что плохо), и вы, вероятно, потеряете временную локальность (что тоже плохо).