Risposte:
Non è necessario scrivere alcun codice. Usa il metodo MoreLINQ Batch, che raggruppa la sequenza di origine in bucket dimensionati (MoreLINQ è disponibile come pacchetto NuGet che puoi installare):
int size = 10;
var batches = sequence.Batch(size);
Che è implementato come:
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)
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
int maxItems)
{
return items.Select((item, inx) => new { item, inx })
.GroupBy(x => x.inx / maxItems)
.Select(g => g.Select(x => x.item));
}
}
e l'utilizzo sarebbe:
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
foreach(var batch in list.Batch(3))
{
Console.WriteLine(String.Join(",",batch));
}
PRODUZIONE:
0,1,2
3,4,5
6,7,8
9
GroupBy
avviata l'enumerazione, non è necessario enumerare completamente la sua fonte? Ciò perde la valutazione pigra della fonte e quindi, in alcuni casi, tutti i vantaggi del batch!
Se inizi con sequence
definito come un IEnumerable<T>
e sai che può essere enumerato in modo sicuro più volte (ad esempio perché è un array o un elenco), puoi semplicemente utilizzare questo semplice modello per elaborare gli elementi in batch:
while (sequence.Any())
{
var batch = sequence.Take(10);
sequence = sequence.Skip(10);
// do whatever you need to do with each batch here
}
Tutto quanto sopra funziona in modo terribile con lotti di grandi dimensioni o spazio di memoria insufficiente. Ho dovuto scrivere il mio che verrà pipeline (non notare alcun accumulo di articoli da nessuna parte):
public static class BatchLinq {
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (IEnumerator<T> enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
yield return TakeIEnumerator(enumerator, size);
}
private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
int i = 0;
do
yield return source.Current;
while (++i < size && source.MoveNext());
}
}
Modifica: problema noto con questo approccio è che ogni batch deve essere enumerato ed enumerato completamente prima di passare al batch successivo. Ad esempio questo non funziona:
//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Si tratta di un'implementazione di Batch a una funzione completamente pigra, a basso overhead e che non fa alcun accumulo. Basato su (e risolto problemi nella) soluzione di Nick Whaley con l'aiuto di EricRoller.
L'iterazione proviene direttamente dall'IEnumerable sottostante, quindi gli elementi devono essere enumerati in ordine rigoroso e non è necessario accedervi più di una volta. Se alcuni elementi non vengono consumati in un ciclo interno, vengono scartati (e il tentativo di accedervi di nuovo tramite un iteratore salvato verrà generato InvalidOperationException: Enumeration already finished.
).
Puoi testare un campione completo su .NET Fiddle .
public static class BatchLinq
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (var enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
{
int i = 0;
// Batch is a local function closing over `i` and `enumerator` that
// executes the inner batch enumeration
IEnumerable<T> Batch()
{
do yield return enumerator.Current;
while (++i < size && enumerator.MoveNext());
}
yield return Batch();
while (++i < size && enumerator.MoveNext()); // discard skipped items
}
}
}
done
semplicemente chiamando sempre e.Count()
dopo yield return e
. Dovresti riorganizzare il ciclo in BatchInner per non richiamare il comportamento non definito source.Current
se i >= size
. Ciò eliminerà la necessità di allocare un nuovo BatchInner
per ogni batch.
i
quindi questo non è necessariamente più efficiente della definizione di una classe separata, ma credo sia un po 'più pulito.
Mi chiedo perché nessuno abbia mai pubblicato una soluzione for-loop della vecchia scuola. Eccone uno:
List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
var batch = source.Skip(i).Take(batchsize);
}
Questa semplicità è possibile perché il metodo Take:
... enumera
source
e restituisce elementi finché glicount
elementi non sono stati restituiti osource
non contiene più elementi. Secount
supera il numero di elementi insource
,source
vengono restituiti tutti gli elementi di
Disclaimer:
L'uso di Skip and Take all'interno del ciclo significa che l'enumerabile verrà enumerato più volte. Questo è pericoloso se l'enumerabile viene differito. Potrebbe comportare più esecuzioni di una query di database, una richiesta Web o la lettura di un file. Questo esempio è esplicitamente per l'utilizzo di un elenco che non è differito, quindi è un problema minore. È ancora una soluzione lenta poiché skip enumererà la raccolta ogni volta che viene chiamata.
Questo può anche essere risolto utilizzando il GetRange
metodo, ma richiede un calcolo aggiuntivo per estrarre un possibile lotto di riposo:
for (int i = 0; i < source.Count; i += batchsize)
{
int remaining = source.Count - i;
var batch = remaining > batchsize ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}
Ecco un terzo modo per gestirlo, che funziona con 2 loop. Ciò garantisce che la raccolta venga enumerata solo 1 volta !:
int batchsize = 10;
List<int> batch = new List<int>(batchsize);
for (int i = 0; i < source.Count; i += batchsize)
{
// calculated the remaining items to avoid an OutOfRangeException
batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
for (int j = i; j < i + batchsize; j++)
{
batch.Add(source[j]);
}
batch.Clear();
}
Skip
e Take
all'interno del ciclo significa che l'enumerabile verrà enumerato più volte. Questo è pericoloso se l'enumerabile viene differito. Potrebbe comportare più esecuzioni di una query di database, una richiesta Web o la lettura di un file. Nel tuo esempio hai un List
che non è differito, quindi è un problema minore.
Stesso approccio di MoreLINQ, ma utilizzando List invece di Array. Non ho eseguito il benchmarking, ma la leggibilità è più importante per alcune persone:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
List<T> batch = new List<T>();
foreach (var item in source)
{
batch.Add(item);
if (batch.Count >= size)
{
yield return batch;
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch;
}
}
size
parametro al tuo new List
per ottimizzarne le dimensioni.
batch.Clear();
batch = new List<T>();
Ecco un tentativo di miglioramento delle implementazioni pigre di Nick Whaley ( link ) e di infogulch ( link ) Batch
. Questo è rigoroso. Enumeri i batch nell'ordine corretto o ottieni un'eccezione.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
using (var enumerator = source.GetEnumerator())
{
int i = 0;
while (enumerator.MoveNext())
{
if (i % size != 0) throw new InvalidOperationException(
"The enumeration is out of order.");
i++;
yield return GetBatch();
}
IEnumerable<TSource> GetBatch()
{
while (true)
{
yield return enumerator.Current;
if (i % size == 0 || !enumerator.MoveNext()) break;
i++;
}
}
}
}
Ed ecco Batch
un'implementazione pigra per sorgenti di tipo IList<T>
. Questo non impone restrizioni sull'enumerazione. I batch possono essere enumerati parzialmente, in qualsiasi ordine e più di una volta. Tuttavia, la restrizione di non modificare la raccolta durante l'enumerazione è ancora in vigore. Ciò si ottiene effettuando una chiamata fittizia a enumerator.MoveNext()
prima di cedere qualsiasi blocco o elemento. Lo svantaggio è che l'enumeratore non viene smaltito, poiché non è noto quando l'enumerazione finirà.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IList<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
var enumerator = source.GetEnumerator();
for (int i = 0; i < source.Count; i += size)
{
enumerator.MoveNext();
yield return GetChunk(i, Math.Min(i + size, source.Count));
}
IEnumerable<TSource> GetChunk(int from, int toExclusive)
{
for (int j = from; j < toExclusive; j++)
{
enumerator.MoveNext();
yield return source[j];
}
}
}
Mi unisco a questo molto tardi ma ho trovato qualcosa di più interessante.
Quindi possiamo usare qui Skip
e Take
per prestazioni migliori.
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}
public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
{
return items.Skip(skip).Take(take);
}
}
Successivamente ho controllato con 100000 record. Solo il looping richiede più tempo in caso diBatch
Codice dell'applicazione console.
static void Main(string[] args)
{
List<string> Ids = GetData("First");
List<string> Ids2 = GetData("tsriF");
Stopwatch FirstWatch = new Stopwatch();
FirstWatch.Start();
foreach (var batch in Ids2.Batch(5000))
{
// Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
}
FirstWatch.Stop();
Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());
Stopwatch Second = new Stopwatch();
Second.Start();
int Length = Ids2.Count;
int StartIndex = 0;
int BatchSize = 5000;
while (Length > 0)
{
var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
// Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
Length = Length - BatchSize;
StartIndex += BatchSize;
}
Second.Stop();
Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
Console.ReadKey();
}
static List<string> GetData(string name)
{
List<string> Data = new List<string>();
for (int i = 0; i < 100000; i++)
{
Data.Add(string.Format("{0} {1}", name, i.ToString()));
}
return Data;
}
Il tempo impiegato è così.
Primo - 00: 00: 00.0708, 00: 00: 00.0660
Secondo (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008
GroupBy
enumera completamente prima di produrre una singola riga. Questo non è un buon modo per eseguire il batch.
foreach (var batch in Ids2.Batch(5000))
a var gourpBatch = Ids2.Batch(5000)
e controllare i risultati a tempo. o aggiungi elenco a var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
sarei interessato se i tuoi risultati per la temporizzazione cambiassero.
Quindi, con un cappello funzionale, questo sembra banale ... ma in C # ci sono alcuni svantaggi significativi.
probabilmente lo vedresti come un unfold di IEnumerable (google e probabilmente finirai in alcuni documenti Haskell, ma potrebbero esserci alcune cose di F # usando unfold, se conosci F #, strizza gli occhi ai documenti Haskell e farà senso).
Unfold è correlato a fold ("aggregate") tranne che invece di iterare attraverso l'input IEnumerable, itera attraverso le strutture dei dati di output (è una relazione simile tra IEnumerable e IObservable, infatti penso che IObservable implementa un "unfold" chiamato generate. ..)
comunque prima hai bisogno di un metodo unfold, penso che funzioni (sfortunatamente alla fine farà saltare lo stack per grandi "liste" ... puoi scriverlo tranquillamente in F # usando yield! piuttosto che concat);
static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
{
var maybeNewSeedAndElement = f(seed);
return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
}
questo è un po 'ottuso perché C # non implementa alcune delle cose che i linguaggi funzionali danno per scontato ... ma fondamentalmente prende un seme e quindi genera una risposta "Forse" dell'elemento successivo in IEnumerable e il seme successivo (Forse non esiste in C #, quindi abbiamo usato IEnumerable per simularlo) e concatena il resto della risposta (non posso garantire la complessità "O (n?)" di questo).
Dopo averlo fatto, allora;
static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
{
return Unfold(ys =>
{
var head = ys.Take(n);
var tail = ys.Skip(n);
return head.Take(1).Select(_ => Tuple.Create(tail, head));
},
xs);
}
sembra tutto abbastanza pulito ... prendi gli elementi "n" come l'elemento "successivo" in IEnumerable, e la "coda" è il resto dell'elenco non elaborato.
se non c'è niente nella testa ... sei finito ... restituisci "Nothing" (ma simulato come un IEnumerable> vuoto) ... altrimenti restituisci l'elemento head e la coda da elaborare.
probabilmente puoi farlo usando IObservable, probabilmente c'è già un metodo simile a "Batch", e probabilmente puoi usarlo.
Se il rischio di overflow dello stack è preoccupante (probabilmente dovrebbe), allora dovresti implementarlo in F # (e probabilmente c'è già qualche libreria F # (FSharpX?) Con questo).
(Ho fatto solo alcuni test rudimentali su questo, quindi potrebbero esserci degli strani bug lì dentro).
Ho scritto un'implementazione IEnumerable personalizzata che funziona senza linq e garantisce una singola enumerazione sui dati. Inoltre, esegue tutto ciò senza richiedere elenchi o array di backup che causano esplosioni di memoria su set di dati di grandi dimensioni.
Ecco alcuni test di base:
[Fact]
public void ShouldPartition()
{
var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var data = ints.PartitionByMaxGroupSize(3);
data.Count().Should().Be(4);
data.Skip(0).First().Count().Should().Be(3);
data.Skip(0).First().ToList()[0].Should().Be(0);
data.Skip(0).First().ToList()[1].Should().Be(1);
data.Skip(0).First().ToList()[2].Should().Be(2);
data.Skip(1).First().Count().Should().Be(3);
data.Skip(1).First().ToList()[0].Should().Be(3);
data.Skip(1).First().ToList()[1].Should().Be(4);
data.Skip(1).First().ToList()[2].Should().Be(5);
data.Skip(2).First().Count().Should().Be(3);
data.Skip(2).First().ToList()[0].Should().Be(6);
data.Skip(2).First().ToList()[1].Should().Be(7);
data.Skip(2).First().ToList()[2].Should().Be(8);
data.Skip(3).First().Count().Should().Be(1);
data.Skip(3).First().ToList()[0].Should().Be(9);
}
Il metodo di estensione per partizionare i dati.
/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>.
/// </summary>
public static class EnumerableExtender
{
/// <summary>
/// Splits an enumerable into chucks, by a maximum group size.
/// </summary>
/// <param name="source">The source to split</param>
/// <param name="maxSize">The maximum number of items per group.</param>
/// <typeparam name="T">The type of item to split</typeparam>
/// <returns>A list of lists of the original items.</returns>
public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
{
return new SplittingEnumerable<T>(source, maxSize);
}
}
Questa è la classe di implementazione
using System.Collections;
using System.Collections.Generic;
internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> backing;
private readonly int maxSize;
private bool hasCurrent;
private T lastItem;
public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
{
this.backing = backing;
this.maxSize = maxSize;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
return new Enumerator(this, this.backing.GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class Enumerator : IEnumerator<IEnumerable<T>>
{
private readonly SplittingEnumerable<T> parent;
private readonly IEnumerator<T> backingEnumerator;
private NextEnumerable current;
public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
{
this.parent = parent;
this.backingEnumerator = backingEnumerator;
this.parent.hasCurrent = this.backingEnumerator.MoveNext();
if (this.parent.hasCurrent)
{
this.parent.lastItem = this.backingEnumerator.Current;
}
}
public bool MoveNext()
{
if (this.current == null)
{
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
else
{
if (!this.current.IsComplete)
{
using (var enumerator = this.current.GetEnumerator())
{
while (enumerator.MoveNext())
{
}
}
}
}
if (!this.parent.hasCurrent)
{
return false;
}
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public IEnumerable<T> Current
{
get { return this.current; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
private class NextEnumerable : IEnumerable<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly IEnumerator<T> backingEnumerator;
private int currentSize;
public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
{
this.splitter = splitter;
this.backingEnumerator = backingEnumerator;
}
public bool IsComplete { get; private set; }
public IEnumerator<T> GetEnumerator()
{
return new NextEnumerator(this.splitter, this, this.backingEnumerator);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class NextEnumerator : IEnumerator<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly NextEnumerable parent;
private readonly IEnumerator<T> enumerator;
private T currentItem;
public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
{
this.splitter = splitter;
this.parent = parent;
this.enumerator = enumerator;
}
public bool MoveNext()
{
this.parent.currentSize += 1;
this.currentItem = this.splitter.lastItem;
var hasCcurent = this.splitter.hasCurrent;
this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;
if (this.parent.IsComplete)
{
return false;
}
if (hasCcurent)
{
var result = this.enumerator.MoveNext();
this.splitter.lastItem = this.enumerator.Current;
this.splitter.hasCurrent = result;
}
return hasCcurent;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public T Current
{
get { return this.currentItem; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
}
}
So che tutti usavano sistemi complessi per fare questo lavoro, e davvero non capisco perché. Take and skip consentirà tutte quelle operazioni utilizzando la Func<TSource,Int32,TResult>
funzione di selezione comune con trasformazione. Piace:
public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());
source
verrà ripetuto molto spesso.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
Solo un'altra implementazione di una riga. Funziona anche con un elenco vuoto, in questo caso si ottiene una raccolta batch di dimensioni zero.
var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;
var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));
Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });
Un altro modo è usare l' operatore Rx Buffer
//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;
var observableBatches = anAnumerable.ToObservable().Buffer(size);
var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
GetAwaiter().GetResult()
. Questo è un odore di codice per il codice sincrono che chiama forzatamente il codice asincrono.
static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
{
return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
select @group.Select(xi => xi.x);
}