Czy ktoś może zasugerować sposób tworzenia partii o określonym rozmiarze w linq?
Idealnie chciałbym móc wykonywać operacje na fragmentach o określonej konfigurowalnej ilości.
Nie musisz pisać żadnego kodu. Użyj metody MoreLINQ Batch, która grupuje sekwencję źródłową w zasobniki o rozmiarach (MoreLINQ jest dostępny jako pakiet NuGet, który można zainstalować):
int size = 10;
var batches = sequence.Batch(size);
Który jest realizowany jako:
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new TSource[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (bucket != null && count > 0)
yield return bucket.Take(count).ToArray();
}
Batch(new int[] { 1, 2 }, 1000000)
a użycie byłoby następujące:
WYNIK:
źródło
GroupBy
rozpoczęciu wyliczania nie musi w pełni wyliczać źródła? Traci to leniwą ocenę źródła, a tym samym, w niektórych przypadkach, wszystkie korzyści z grupowania!Jeśli zaczynasz od
sequence
zdefiniowanego jako anIEnumerable<T>
i wiesz, że można go bezpiecznie wyliczyć wiele razy (np. Ponieważ jest to tablica lub lista), możesz po prostu użyć tego prostego wzorca do przetwarzania elementów w partiach:źródło
Wszystkie powyższe działają strasznie przy dużych partiach lub małej ilości pamięci. Musiałem napisać własny, który będzie pipeline (nie zauważaj nigdzie gromadzenia się przedmiotów):
Edycja: Znanym problemem związanym z tym podejściem jest to, że każda partia musi zostać w pełni wyliczona i wyliczona przed przejściem do następnej partii. Na przykład to nie działa:
źródło
Jest to w pełni leniwa, niskobudżetowa, jednofunkcyjna implementacja usługi Batch, która nie wykonuje żadnej akumulacji. Na podstawie (i rozwiązuje problemy) rozwiązania Nicka Whaleya z pomocą EricRoller.
Iteracja pochodzi bezpośrednio z bazowego IEnumerable, więc elementy muszą być wyliczane w ścisłej kolejności i dostępne nie więcej niż raz. Jeśli niektóre elementy nie są zużywane w pętli wewnętrznej, są odrzucane (a próba ponownego dostępu do nich za pomocą zapisanego iteratora spowoduje wyświetlenie
InvalidOperationException: Enumeration already finished.
).Możesz przetestować pełną próbkę w .NET Fiddle .
źródło
done
zawsze dzwoniące.Count()
poyield return e
. Będziesz musiał zmienić układ pętli w BatchInner, aby nie wywoływać niezdefiniowanego zachowania,source.Current
jeślii >= size
. Eliminuje to konieczność przydzielania nowegoBatchInner
dla każdej partii.i
więc niekoniecznie jest to bardziej wydajne niż definiowanie oddzielnej klasy, ale myślę, że jest trochę czystsze.Zastanawiam się, dlaczego nikt nigdy nie opublikował starej szkoły rozwiązania pętli for. Tutaj jest jeden:
Ta prostota jest możliwa, ponieważ metoda Take:
Zrzeczenie się:
Korzystanie z opcji Skip and Take w pętli oznacza, że wyliczalne będą wyliczane wiele razy. Jest to niebezpieczne, jeśli wyliczalne jest odroczone. Może to spowodować wielokrotne wykonanie zapytania do bazy danych, żądania internetowego lub odczytu pliku. Ten przykład wyraźnie dotyczy użycia listy, która nie jest odroczona, więc stanowi mniejszy problem. Jest to nadal powolne rozwiązanie, ponieważ skip wylicza kolekcję za każdym razem, gdy jest wywoływana.
Można to również rozwiązać za pomocą tej
GetRange
metody, ale wymaga to dodatkowych obliczeń, aby wyodrębnić ewentualną pozostałą partię:Oto trzeci sposób rozwiązania tego problemu, który działa z 2 pętlami. Dzięki temu kolekcja zostanie wyliczona tylko 1 raz !:
źródło
Skip
iTake
wewnątrz pętli oznacza, że wyliczalne będą wyliczane wiele razy. Jest to niebezpieczne, jeśli wyliczalne jest odroczone. Może to spowodować wielokrotne wykonanie zapytania do bazy danych, żądania internetowego lub odczytu pliku. W twoim przykładzie masz,List
który nie jest odroczony, więc jest to mniejszy problem.To samo podejście co MoreLINQ, ale używa List zamiast Array. Nie robiłem testów porównawczych, ale czytelność ma dla niektórych większe znaczenie:
źródło
size
parametr do swojego,new List
aby zoptymalizować jego rozmiar.batch.Clear();
zbatch = new List<T>();
Oto próba ulepszenia leniwych implementacji Nicka Whaleya ( link ) i infogulch ( link )
Batch
. Ten jest surowy. Możesz wyliczyć partie w odpowiedniej kolejności lub otrzymasz wyjątek.A oto leniwa
Batch
implementacja dla źródeł typuIList<T>
. Ten nie nakłada żadnych ograniczeń na wyliczenie. Partie można wyliczyć częściowo, w dowolnej kolejności i więcej niż raz. Jednak nadal obowiązuje ograniczenie dotyczące nie modyfikowania kolekcji podczas wyliczania. Osiąga się to, wykonując fikcyjne wywołanieenumerator.MoveNext()
przed uzyskaniem dowolnego fragmentu lub elementu. Wadą jest to, że moduł wyliczający pozostaje niewykorzystany, ponieważ nie wiadomo, kiedy wyliczenie się zakończy.źródło
Dołączam do tego bardzo późno, ale znalazłem coś bardziej interesującego.
Więc możemy użyć tutaj
Skip
iTake
dla lepszej wydajności.Następnie sprawdziłem ze 100000 rekordami. Tylko zapętlenie zajmuje więcej czasu w przypadku
Batch
Kod aplikacji konsolowej.
Czas zajęty jest taki.
Pierwsza - 00: 00: 00.0708, 00: 00: 00.0660
Drugi (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008
źródło
GroupBy
w pełni wylicza, zanim utworzy pojedynczy wiersz. To nie jest dobry sposób na przetwarzanie wsadowe.foreach (var batch in Ids2.Batch(5000))
sięvar gourpBatch = Ids2.Batch(5000)
i sprawdzić wyniki czasowe. lub dodaj listę tolist do.var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
Byłbym zainteresowany, gdyby zmieniły się Twoje wyniki.Tak więc z funkcjonalnym kapeluszem wydaje się to trywialne ... ale w C # są pewne istotne wady.
prawdopodobnie zobaczysz to jako rozwinięcie IEnumerable (wygoogluj to i prawdopodobnie skończysz w niektórych dokumentach Haskell, ale mogą być jakieś rzeczy F # używające rozwijania, jeśli znasz F #, mruż oczy na dokumentach Haskell i sprawi, że sens).
Unfold jest powiązany z fold ("agregacja"), z wyjątkiem tego, że zamiast iteracji przez dane wejściowe IEnumerable, iteruje przez struktury danych wyjściowych (jest to podobna relacja między IEnumerable i IObservable, w rzeczywistości myślę, że IObservable implementuje "rozwijanie" o nazwie generuj. ..)
w każdym razie najpierw potrzebujesz metody rozwijania, myślę, że to działa (niestety ostatecznie wysadzi stos dla dużych "list" ... możesz to bezpiecznie napisać w F # używając yield! zamiast concat);
jest to trochę tępe, ponieważ C # nie implementuje niektórych rzeczy, które langauges funkcjonalne przyjmują za pewnik ... ale w zasadzie pobiera ziarno, a następnie generuje odpowiedź "Może" dla następnego elementu w IEnumerable i następnego ziarna (Może nie istnieje w C #, więc użyliśmy IEnumerable, aby go sfałszować) i konkatenujemy resztę odpowiedzi (nie mogę ręczyć za złożoność „O (n?)”).
Kiedy już to zrobisz;
wszystko wygląda całkiem czysto… bierzesz „n” elementów jako „następny” element w IEnumerable, a „ogon” to reszta nieprzetworzonej listy.
jeśli nie ma nic w głowie ... to koniec ... zwracasz „Nothing” (ale udaje pustego IEnumerable>) ... w przeciwnym razie zwracasz element head i tail do przetworzenia.
prawdopodobnie możesz to zrobić za pomocą IObservable, prawdopodobnie istnieje już metoda podobna do "Batch" i prawdopodobnie możesz jej użyć.
Jeśli ryzyko przepełnienia stosu powoduje obawy (prawdopodobnie powinno), to powinieneś zaimplementować w F # (i prawdopodobnie jest już jakaś biblioteka F # (FSharpX?)).
(Zrobiłem tylko podstawowe testy tego, więc mogą tam być dziwne błędy).
źródło
Napisałem niestandardową implementację IEnumerable, która działa bez linq i gwarantuje pojedyncze wyliczenie danych. Osiąga to również wszystko bez konieczności tworzenia list zapasowych lub tablic, które powodują eksplozje pamięci w dużych zestawach danych.
Oto kilka podstawowych testów:
Metoda rozszerzenia do partycjonowania danych.
To jest klasa implementująca
źródło
Wiem, że wszyscy używali skomplikowanych systemów do tej pracy i naprawdę nie rozumiem dlaczego. Take and skip pozwoli na wszystkie te operacje przy użyciu wspólnego wyboru z
Func<TSource,Int32,TResult>
funkcją transformacji. Lubić:źródło
source
będą często powtarzane.Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.Po prostu kolejna implementacja jednej linii. Działa nawet z pustą listą, w tym przypadku otrzymasz kolekcję partii o zerowym rozmiarze.
źródło
Innym sposobem jest użycie operatora Rx Buffer
źródło
GetAwaiter().GetResult()
. Jest to zapach kodu dla kodu synchronicznego, który wymusza wywołanie kodu asynchronicznego.źródło