Това е част от поредица от статии:
- Изброяване в .NET
- Изброяване в .NET II — Count()
- Изброяване в .NET III — Enumerable.Empty‹T›()
- Изброяване в .NET IV — Намиране на елемент
- Изброяване в .NET V — ToList() или не ToList()
ToList()
Много често откривам използването на ToList()
в края на всяка LINQ заявка. През повечето време това не е необходимо и може да има огромно влияние върху производителността.
Нека анализираме малък пример:
Този код записва в конзолата четните числа между 0 и 10. Можете да видите в SharpLab.io, че това върши работа.
Нека разширим ToList()
до еквивалентен код:
ЗАБЕЛЕЖКА:
ToList()
иToArray()
реализациите имат някои страхотни оптимизации, но няма да разберете какво ги задейства, освен ако не погледнете в кода им.
Проверете в SharpLab.io дали резултатът е същият, но това ли наистина искахте? ToList()
скрива допълнително List<T>
разпределение, един foreach
цикъл и копие на всеки елемент в списъка.
Всъщност ToList()
не е необходимо за този случай...
Проверете в SharpLab.io дали кодът без ToList()
извежда абсолютно същото.
производителност
Изпълнението на кода на BenchmarkDotNet за диапазони от 0, 500 и 1000 елемента показва следните резултати.
ЗАБЕЛЕЖКА: За бенчмарка изчислявам общата сума на елементите на последователността, вместо да извеждам към конзолата.
ЗАБЕЛЕЖКА:
ToArray()
е подобен наToList()
, но връща масив вместо списък и го включих в сравнителните тестове.
Не е изненадващо, че всички реализации са O(n), което означава, че времето за обработка се увеличава с броя на елементите в последователността.
Изненадващо, ToArray()
е по-бърз, отколкото да не използва никакво преобразуване. Трябва да задейства една от оптимизациите.
По отношение на паметта, това е мястото, където неизползването на преобразуване прави огромна разлика и не е възможно да се оптимизират преобразуванията допълнително, тъй като данните трябва да са в паметта. За това са...
Преобразуването изисква заделяне на памет на куп, като необходимото количество нараства директно с броя на елементите в последователността.
Разпределенията на купчина ще задействат събирането на боклука. Ако разпределите малки количества и ги запазите за кратък период от време, те ще бъдат обработени от колекцията Gen 0, която е бърза, но не е безплатна. Ако разпределите голямо количество (›85 000 байта), те ще отидат директно в LOH (Large Object Heap), причинявайки фрагментирането му и забавяйки го.
Заключение
ToList()
и ToArray()
ще разпределят хийп памет, задействайки GC по-често и рискувайки да получат OutOfMemoryException
, когато проектът се мащабира.
Трябва да ги използвате само когато е строго необходимо!