Często spotykam się z przypadkiem, w którym chcę sprawdzić zapytanie dokładnie tam, gdzie je zadeklaruję. Dzieje się tak zwykle dlatego, że muszę wielokrotnie iterować i jest to kosztowne. Na przykład:
string raw = "...";
var lines = (from l in raw.Split('\n')
let ll = l.Trim()
where !string.IsNullOrEmpty(ll)
select ll).ToList();
To działa dobrze. Ale jeśli nie zamierzam modyfikować wyniku, mogę równie dobrze zadzwonić ToArray()
zamiast ToList()
.
Zastanawiam się jednak, czy ToArray()
jest realizowane przez pierwsze wywołanie ToList()
i dlatego jest mniej wydajne pod względem pamięci niż zwykłe wywołanie ToList()
.
Czy jestem szalony? Czy powinienem po prostu zadzwonić ToArray()
- bezpiecznie, mając świadomość, że pamięć nie zostanie przydzielona dwukrotnie?
.net
linq
performance
Frank Krueger
źródło
źródło
Odpowiedzi:
Chyba że potrzebujesz tablicy, aby spełnić inne ograniczenia, których powinieneś użyć
ToList
. W większości scenariuszyToArray
przydzieli więcej pamięci niżToList
.Oba używają tablic do przechowywania, ale
ToList
mają bardziej elastyczne ograniczenie. Wymaga tablicy co najmniej tak dużej, jak liczba elementów w kolekcji. Jeśli tablica jest większa, nie stanowi to problemu.ToArray
Wymaga jednak, aby tablica była dokładnie dopasowana do liczby elementów.Aby spełnić to ograniczenie,
ToArray
często dokonuje się jeszcze jeden przydział niżToList
. Kiedy ma już wystarczająco dużą tablicę, przydziela tablicę o dokładnie odpowiednim rozmiarze i kopiuje elementy z powrotem do tej tablicy. Można tego uniknąć tylko wtedy, gdy algorytm powiększania dla tablicy zbiega się z liczbą elementów, które muszą być przechowywane (zdecydowanie w mniejszości).EDYTOWAĆ
Kilka osób zapytało mnie o konsekwencje posiadania dodatkowej nieużywanej pamięci w
List<T>
wartości.To jest ważny problem. Jeśli utworzona kolekcja jest długowieczna, nigdy nie jest modyfikowana po utworzeniu i ma duże szanse na wylądowanie na stosie Gen2, lepiej być może skorzystasz z dodatkowej alokacji z
ToArray
góry.Ogólnie jednak uważam, że jest to rzadszy przypadek. Znacznie częściej zdarza się widzieć wiele
ToArray
połączeń, które są natychmiast przekazywane do innych krótkotrwałych zastosowań pamięci, w którymToList
to przypadku jest wyraźnie lepsza.Kluczem tutaj jest profilowanie, profilowanie, a następnie profilowanie jeszcze bardziej.
źródło
ToArray
można przydzielić więcej pamięci, jeśli potrzebuje dokładnej wielkości lokalizacji, gdzieToList<>
oczywiście ma to automatyczne rezerwowe lokalizacje. (automatyczne zwiększenie)Różnica wydajności będzie nieznaczna, ponieważ
List<T>
jest implementowana jako tablica o dynamicznym rozmiarze. Wywołanie alboToArray()
(która wykorzystujeBuffer<T>
klasę wewnętrzną do powiększenia tablicy) alboToList()
(która wywołujeList<T>(IEnumerable<T>)
konstruktora) zakończy się kwestią umieszczenia ich w tablicy i powiększenia tablicy, aż dopasuje je wszystkie.Jeśli chcesz konkretnego potwierdzenia tego faktu, sprawdź implementację omawianych metod w Reflector - zobaczysz, że sprowadzają się one do prawie identycznego kodu.
źródło
ToArray()
iToList()
jest to, że ta pierwsza musi przyciąć nadmiar, co wiąże się z skopiowaniem całej tablicy, podczas gdy druga nie przycina nadwyżki, ale używa średnio 25 % więcej pamięci. Będzie to miało wpływ tylko wtedy, gdy typ danych jest dużystruct
. Tylko jedzenie do namysłu.ToList
lubToArray
rozpocznie się od utworzenia małego bufora. Kiedy ten bufor jest wypełniony, podwaja pojemność bufora i kontynuuje. Ponieważ pojemność jest zawsze podwojona, nieużywany bufor będzie zawsze wynosił od 0% do 50%.List
iBuffer
sprawdziICollection
, w którym to przypadku wydajność będzie identyczna.(siedem lat później...)
Kilka innych (dobrych) odpowiedzi skoncentrowało się na mikroskopowych różnicach wydajności, które wystąpią.
Ten post jest tylko uzupełnieniem, aby wspomnieć o semantycznej różnicy, jaka istnieje między
IEnumerator<T>
produkowaną przez tablicę (T[]
) w porównaniu do zwracanej przez aList<T>
.Najlepiej ilustruje to przykład:
Powyższy kod będzie działał bez wyjątku i generuje dane wyjściowe:
To pokazuje, że
IEnumarator<int>
zwrócone przez aint[]
nie śledzi, czy tablica została zmodyfikowana od czasu utworzenia modułu wyliczającego.Zauważ, że zadeklarowałem zmienną lokalną
source
jakoIList<int>
. W ten sposób upewniam się, że kompilator C # nie optymalizujeforeach
instrukcji do czegoś, co jest równoważnefor (var idx = 0; idx < source.Length; idx++) { /* ... */ }
pętli. Jest to coś, co kompilator C # może zrobić, jeśli użyjęvar source = ...;
zamiast tego. W mojej bieżącej wersji środowiska .NET używany tutaj moduł wyliczający jest niepublicznym typem referencyjnym,System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32]
ale oczywiście jest to szczegół implementacyjny.Teraz, jeśli zmienię
.ToArray()
się.ToList()
, mam tylko:po czym następuje
System.InvalidOperationException
wysadzenie w powietrze:Podstawowym modułem wyliczającym w tym przypadku jest publiczny zmienny typ wartości
System.Collections.Generic.List`1+Enumerator[System.Int32]
(IEnumerator<int>
w tym przypadku zawarty w polu, ponieważ używamIList<int>
).Podsumowując, moduł wyliczający utworzony przez
List<T>
śledzenie śledzi, czy lista zmienia się podczas wyliczania, podczas gdy moduł wyliczający wytworzony przezT[]
nie. Więc rozważ tę różnicę, wybierając pomiędzy.ToList()
i.ToArray()
.Ludzie często dodają jeden dodatek
.ToArray()
lub w.ToList()
celu obejścia kolekcji, która śledzi, czy została zmodyfikowana w czasie życia modułu wyliczającego.(Jeśli ktoś chce wiedzieć, jak
List<>
śledzi czy zbiór został zmodyfikowany, nie jest prywatnym polu_version
w tej klasie, która zmienia się za każdym razemList<>
jest aktualizowana.)źródło
Zgadzam się z @mquander, że różnica w wydajności powinna być nieznaczna. Chciałem jednak to sprawdzić, więc zrobiłem to - i jest to nieznaczące.
Każda tablica / lista źródłowa zawierała 1000 elementów. Widać więc, że różnice w czasie i pamięci są znikome.
Mój wniosek: równie dobrze możesz użyć ToList () , ponieważ
List<T>
zapewnia więcej funkcji niż tablica, chyba że kilka bajtów pamięci naprawdę ma dla ciebie znaczenie.źródło
struct
zamiast prymitywnego typu lub klasy.ToList
lubToArray
połączenia, a nie ich numeracjęIEnumerable
. List <T> .ToList () nadal tworzy nową Listę <T> - nie tylko „zwraca to”.ToArray()
iToList()
różnią się zbytnio, gdy są dostarczane zICollection<T>
parametrem - wykonują tylko jedną alokację i jedną operację kopiowania. ZarównoList<T>
iArray
implementujICollection<T>
, więc twoje testy porównawcze w ogóle nie są ważne..Select(i => i)
aby uniknąćICollection<T>
problemu z implementacją, i zawiera grupę kontrolną, aby zobaczyć, ile czasu zajmuje iteracja źródłaIEnumerable<>
.ToList()
jest zwykle preferowany, jeśli używasz goIEnumerable<T>
(na przykład z ORM). Jeśli długość sekwencji nie jest znana na początku,ToArray()
tworzy kolekcję o dynamicznej długości, taką jak List, a następnie konwertuje ją na tablicę, co zajmuje więcej czasu.źródło
Enumerable.ToArray()
połączenianew Buffer<TSource>(source).ToArray()
. W konstruktorze buforów, jeśli źródło implementuje ICollection, wówczas wywołuje source.CopyTo (items, 0), a następnie .ToArray () zwraca bezpośrednio wewnętrzną tablicę przedmiotów. W takim przypadku nie ma konwersji, która wymaga dodatkowego czasu. Jeśli źródło nie implementuje ICollection, ToArray spowoduje skopiowanie tablicy w celu przycięcia dodatkowych nieużywanych lokalizacji z końca tablicy, jak opisano w komentarzu Scotta Rippeya powyżej.Pamięć zawsze będzie przydzielana dwukrotnie - lub coś w tym stylu. Ponieważ nie można zmienić rozmiaru tablicy, obie metody wykorzystują jakiś mechanizm do gromadzenia danych w rosnącej kolekcji. (Cóż, lista sama w sobie rośnie.)
Lista używa tablicy jako pamięci wewnętrznej i podwaja pojemność w razie potrzeby. Oznacza to, że średnio 2/3 pozycji zostało przeniesionych co najmniej raz, połowa z nich została przydzielona co najmniej dwa razy, połowa tych co najmniej trzy razy i tak dalej. Oznacza to, że każdy przedmiot został średnio przeniesiony 1,3 razy, co nie jest zbyt duże.
Pamiętaj również, że jeśli zbierasz ciągi, sama kolekcja zawiera tylko odniesienia do ciągów, same ciągi nie są ponownie przydzielane.
źródło
Na zewnątrz jest rok 2020 i wszyscy używają .NET Core 3.1, więc postanowiłem uruchomić testy porównawcze z Benchmark.NET.
TL; DR: ToArray () jest bardziej wydajny pod względem wydajności i lepiej przekazuje zamiar, jeśli nie planujesz mutować kolekcji.
Wyniki są następujące:
źródło
ToImmutableArray()
(z pakietu System.Collections.Immutable) 😉Edycja : ostatnia część tej odpowiedzi jest nieprawidłowa. Jednak reszta to wciąż przydatna informacja, więc ją zostawię.
Wiem, że to stary post, ale po zadaniu tego samego pytania i przeprowadzeniu badań znalazłem coś interesującego, co może być warte podzielenia się.
Po pierwsze, zgadzam się z @mquander i jego odpowiedzią. Ma rację mówiąc, że pod względem wydajności oba są identyczne.
Jednak korzystam z Reflectora, aby przyjrzeć się metodom w
System.Linq.Enumerable
przestrzeni nazw rozszerzeń i zauważyłem bardzo powszechną optymalizację.O ile to możliwe,
IEnumerable<T>
źródło jest przesyłane doIList<T>
lub wICollection<T>
celu optymalizacji metody. Na przykład spójrz naElementAt(int)
.Co ciekawe, Microsoft postanowił zoptymalizować tylko
IList<T>
, ale nieIList
. Wygląda na to, że Microsoft woli używaćIList<T>
interfejsu.System.Array
tylko implementujeIList
, więc nie skorzysta z żadnej z tych optymalizacji rozszerzeń.Dlatego uważam, że najlepszą praktyką jest zastosowanie tej
.ToList()
metody.Jeśli użyjesz jednej z metod rozszerzenia lub przekażesz listę innej metodzie, istnieje szansa, że zostanie ona zoptymalizowana pod kątem
IList<T>
.źródło
Stwierdziłem, że brakuje innych testów porównawczych, których ludzie tutaj nie zrobili, więc oto mój problem. Daj mi znać, jeśli znajdziesz coś złego w mojej metodologii.
Możesz pobrać skrypt LINQPad tutaj .
Wyniki:
Poprawiając powyższy kod, odkryjesz, że:
int
s zamiaststring
s.struct
s zamiaststring
s zajmuje ogólnie dużo więcej czasu, ale tak naprawdę nie zmienia zbytnio współczynnika.Jest to zgodne z wnioskami z najczęściej głosowanych odpowiedzi:
ToList()
konsekwentnie działa szybciej i byłby lepszym wyborem, jeśli nie planujesz długo utrzymywać wyników.Aktualizacja
@JonHanna zwrócił uwagę, że w zależności od implementacji
Select
możliwe jest, aby aToList()
lubToArray()
implementacja przewidziała rozmiar wynikowej kolekcji z góry. Zastąpienie.Select(i => i)
powyższego koduWhere(i => true)
wynikami jest obecnie bardzo podobne i jest bardziej prawdopodobne, niezależnie od implementacji platformy .NET.źródło
100000
i użyć jej w celu optymalizacji zarównoToList()
iToArray()
, zeToArray()
jest bardzo nieznacznie lżejszy, ponieważ nie potrzebuje operacji skurczowej musiałaby w przeciwnym razie, które jest jednym miejscemToList()
ma tę zaletę. Przykład w pytaniu wciąż by przegrał, ponieważWhere
nie można wykonać takiego przewidywania wielkości..Select(i => i)
można go zastąpić,.Where(i => true)
aby to poprawić.ToArray()
przewagę), jak i takiego, który nie jest, jak wyżej, i porównuje wyniki.ToArray()
wciąż przegrywa w najlepszym przypadku. ZMath.Pow(2, 15)
elementami jest to (ToList: 700ms, ToArray: 900ms). Dodanie jeszcze jednego elementu powoduje jego zderzenie z (ToList: 925, ToArray: 1350). Zastanawiam się, czyToArray
nadal kopiuje tablicę, nawet jeśli ma już idealny rozmiar? Prawdopodobnie doszli do wniosku, że było to dość rzadkie zjawisko, które nie było warte dodatkowych warunków.Powinieneś oprzeć swoją decyzję na wyborze
ToList
lubToArray
na tym, co najlepiej wybrać projekt. Jeśli chcesz kolekcji, która może być iterowana i dostępna tylko za pomocą indeksu, wybierzToArray
. Jeśli chcesz dodatkowych możliwości dodawania i usuwania z kolekcji później bez większych problemów, zrób toToList
(nie tak naprawdę, że nie możesz dodać do tablicy, ale zwykle nie jest to odpowiednie narzędzie).Jeśli wydajność ma znaczenie, powinieneś również rozważyć, na czym można by działać szybciej. Realistycznie, przyzwyczajenie zadzwonić
ToList
lubToArray
milion razy, ale może pracować na otrzymanej kolekcji milion razy. Pod tym względem[]
jest lepiej, ponieważList<>
wiąże[]
się to z pewnym obciążeniem. Zobacz ten wątek dla porównania wydajności: Który jest bardziej wydajny: List <int> lub int []W moich własnych testach jakiś czas temu znalazłem
ToArray
szybciej. I nie jestem pewien, jak wypaczone były testy. Różnica w wydajności jest jednak tak nieznaczna, że można to zauważyć tylko wtedy, gdy te zapytania są uruchamiane miliony razy w pętli.źródło
Bardzo późna odpowiedź, ale myślę, że będzie to pomocne dla pracowników Google.
Oboje ssają, kiedy stworzyli za pomocą linq. Obie implementują ten sam kod, aby w razie potrzeby zmienić rozmiar bufora .
ToArray
wewnętrznie wykorzystuje klasę do konwersjiIEnumerable<>
na tablicę, przydzielając tablicę 4 elementów. Jeśli to nie wystarczy, podwaja rozmiar, tworząc nową tablicę dwukrotnie powiększając prąd i kopiując do niej tablicę bieżącą. Na koniec przydziela nowy zestaw liczników twoich przedmiotów. Jeśli zapytanie zwróci 129 elementów, ToArray dokona 6 operacji alokacji i operacji kopiowania pamięci, aby utworzyć tablicę 256-elementową, a następnie zostanie zwrócona inna tablica 129. tyle dla wydajności pamięci.ToList robi to samo, ale pomija ostatni przydział, ponieważ możesz dodawać elementy w przyszłości. Lista nie ma znaczenia, czy jest tworzona z zapytania linq czy ręcznie.
do tworzenia Lista jest lepsza z pamięcią, ale gorsza z procesorem, ponieważ lista jest rozwiązaniem ogólnym, każda akcja wymaga dodatkowej kontroli zasięgu poza wewnętrznymi kontrolami zasięgu .net dla tablic.
Jeśli więc zbyt wiele razy będziesz iterował zestaw wyników, to tablice są dobre, ponieważ oznacza to mniej kontroli zasięgu niż listy, a kompilatory ogólnie optymalizują tablice pod kątem dostępu sekwencyjnego.
Przydział inicjalizacji listy może być lepszy, jeśli określisz parametr zdolności podczas jego tworzenia. W takim przypadku tablica przydzieli tablicę tylko raz, zakładając, że znasz rozmiar wyniku.
ToList
linq nie określa przeciążenia, aby je zapewnić, dlatego musimy stworzyć naszą metodę rozszerzenia, która tworzy listę o określonej pojemności, a następnie używaList<>.AddRange
.Aby zakończyć tę odpowiedź, muszę napisać następujące zdania
źródło
List<T>
, ale kiedy tego nie robisz lub kiedy nie możesz, nie możesz nic na to poradzić.To stare pytanie - ale z korzyścią dla użytkowników, którzy się na niego natkną, istnieje również alternatywa „Memoizing” the Enumerable - która powoduje buforowanie i zatrzymanie wielokrotnego wyliczania instrukcji Linq, co jest tym, co ToArray () i ToList () są używane często, mimo że atrybuty kolekcji listy lub tablicy nigdy nie są używane.
Zapamiętywanie jest dostępne w bibliotece RX / System.Interactive i jest wyjaśnione tutaj: Więcej LINQ z System.Interactive
(Z bloga Barta De'Smeta, który jest wysoce zalecany, jeśli często pracujesz z Linq do Objects)
źródło
Jedną z opcji jest dodanie własnej metody rozszerzenia, która zwraca tylko do odczytu
ICollection<T>
. Może to być lepsze niż używanieToList
lubToArray
gdy nie chcesz używać właściwości indeksowania tablicy / listy lub dodawać / usuwać z listy.Testy jednostkowe:
źródło
ToListAsync<T>()
jest preferowany.W Entity Framework 6 obie metody ostatecznie wywołują tę samą metodę wewnętrzną, ale na końcu
ToArrayAsync<T>()
wywołująlist.ToArray()
, która jest implementowana jakoToArrayAsync<T>()
Ma więc pewne koszty ogólne, dlategoToListAsync<T>()
jest preferowane.źródło
Stare pytanie, ale przez cały czas nowi pytający.
Według źródła System.Linq.Enumerable ,
ToList
po prostu zwróć anew List(source)
, aToArray
użyj a,new Buffer<T>(source).ToArray()
aby zwrócić aT[]
.Podczas jazdy na
IEnumerable<T>
jedynym obiektem,ToArray
należy przydzielić pamięci jeden więcej czasu niżToList
. Ale w większości przypadków nie musisz się tym przejmować, ponieważ GC zajmie się usuwaniem śmieci, gdy zajdzie taka potrzeba.Ci, którzy kwestionują to pytanie, mogą uruchomić następujący kod na własnym komputerze, a otrzymasz odpowiedź.
Mam te wyniki na moim komputerze:
Z powodu ograniczenia stosu do liczby znaków w odpowiedzi, listy przykładowe z Grupy 2 i Grupy 3 zostały pominięte.
Jak widać, w rzeczywistości nie jest ważne używanie
ToList
lubToArry
w większości przypadków.Podczas przetwarzania runtime-obliczony
IEnumerable<T>
obiektów, jeśli obciążenie wniesiona przez obliczeń jest ciężki niż alokacji pamięci i skopiować operacjeToList
iToArray
, rozbieżność jest niewielka (C.ToList vs C.ToArray
aS.ToList vs S.ToArray
).Różnicę można zaobserwować tylko w przypadku
IEnumerable<T>
obiektów obliczanych tylko w środowisku wykonawczym (C1.ToList vs C1.ToArray
iS1.ToList vs S1.ToArray
). Ale różnica bezwzględna (<60 ms) jest wciąż akceptowalna dla miliona małych obiektówIEnumerable<T>
. W rzeczywistości różnica decyduje realizacjiEnumerator<T>
odIEnumerable<T>
. Tak więc, jeśli twój program jest naprawdę bardzo wrażliwy na ten temat, musisz profilować, profilować, profilować ! W końcu prawdopodobnie zauważysz, że wąskie gardło nie jest włączoneToList
lubToArray
, ale szczegół rachmistrzów.A wynik
C2.ToList vs C2.ToArray
iS2.ToList vs S2.ToArray
pokazuje, że tak naprawdę nie trzeba się tym przejmowaćToList
ani obiektamiToArray
obliczanymi poza środowiskiem uruchomieniowymICollection<T>
.Oczywiście, to tylko wyniki na moim komputerze, faktyczny czas spędzony na tych operacjach na innym komputerze nie będzie taki sam, możesz dowiedzieć się na swoim komputerze za pomocą powyższego kodu.
Jedynym powodem, dla którego musisz dokonać wyboru, jest to, że masz określone potrzeby
List<T>
lubT[]
, jak opisano w odpowiedzi @Jeppe Stig Nielsen .źródło
Dla wszystkich zainteresowanych wykorzystaniem tego wyniku w innym Linq-sql, takim jak
wówczas generowany kod SQL jest taki sam, niezależnie od tego, czy użyto listy, czy tablicy dla myListOrArray. Teraz wiem, że niektórzy mogą zapytać, dlaczego nawet wyliczyć przed tą instrukcją, ale istnieje różnica między SQL wygenerowanym z IQueryable vs (List lub Array).
źródło