Comment concaténer d'énormes listes sans doubler la mémoire?
Tenez compte de l'extrait suivant:
Initial memory size: 12 MB Memory size after lists initialization: 2014 MB Memory size after lists concatenation: 4039 MB
Le résultat est:
Console.WriteLine($"Initial memory size: {Process.GetCurrentProcess().WorkingSet64 /1024 /1024} MB");
int[] a = Enumerable.Range(0, 1000 * 1024 * 1024 / 4).ToArray();
int[] b = Enumerable.Range(0, 1000 * 1024 * 1024 / 4).ToArray();
Console.WriteLine($"Memory size after lists initialization: {Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024} MB");
List<int> concat = new List<int>();
concat.AddRange(a.Skip(500 * 1024 * 1024 / 4));
concat.AddRange(b.Skip(500 * 1024 * 1024 / 4));
Console.WriteLine($"Memory size after lists concatenation: {Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024} MB");
5 Réponses :
Je peux vous suggérer d'effectuer quelques optimisations:
Initialise a et b comme IEnumerable sans appeler la méthode ToArray ()
IEnumerable<int> concat = a.Skip(500 * 1024 * 1024 / 4).Concat(b.Skip(500 * 1024 * 1024 / 4));
int search = concat.Count(i => i % 2 == 0);
Console.WriteLine($"Search result: {search}");
Initialiser concat avec une capacité connue
Initial memory size: 12 MB Memory size after lists initialization: 13 MB Memory size after lists concatenation: 1021 MB
En conséquence, j'obtiens le résultat suivant:
List<int> concat = new List<int>(size);
Si vous voulez simplement rechercher quelque chose en concaténation, vous pouvez le faire comme ceci sans allocations supplémentaires:
int size = 1000 * 1024 * 1024 / 4; IEnumerable<int> a = Enumerable.Range(0, size); IEnumerable<int> b = Enumerable.Range(0, size);
Ils sont persistants. J'ai juste besoin de les concaténer, de faire quelques recherches, puis de supprimer la liste concaténée
Si vous avez seulement besoin de faire quelques recherches, pourquoi avez-vous besoin de concaténer en premier lieu? Recherchez les deux tableaux séparément.
Il se peut que ce que vous recherchez puisse relier les deux tableaux. Si tel est le cas, pour faciliter les choses et ne pas payer le prix de la mémoire, implémentez simplement un wrapper qui simule l'opération mais ne l'exécute pas réellement:
sealed class Concatenated<T>: IReadOnlyList<T> { public static Concatenated<T> Concatenate<T>( IReadOnlyList<T> first, IReadOnlyList<T> second) => new ConcatenatedArray<T>(first, second); private readonly IReadOnlyList<T> first, second; private Concatenated( IReadOnlyList<T> first, IReadOnlyList<T> second) { this.first = first; this.second = second; } public T this[int index] => index < first.Length ? first[index]: second[index - first.Length]; public int Count => first.Length + second.Length; public IEnumerator<T> GetEnumerator() { foreach (var f in first) yield return f; foreach (var s in second) yield return s; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
Si vous avez besoin d'une List, vous ne pouvez pas le faire. Une List contient toujours ses données directement, donc au moment où vous avez deux tableaux avec (disons) 100 éléments, et une liste qui a été créée en concaténant ces deux, vous avez 400 éléments indépendants. Vous ne pouvez pas changer cela.
Ce que vous recherchez est un moyen de ne pas créer une copie indépendante des données. Si vous faites simplement une recherche (comme cela ressemble à dans les commentaires), vous pouvez simplement utiliser un IEnumerable créé avec LINQ:
IEnumerable<int> concat = a.Concat(b);
Si vous avez besoin de quelque chose comme un IReadOnlyList ou même un IList, vous pouvez implémenter ces interfaces vous-même pour créer un adaptateur sur plusieurs tableaux J'aurais probablement besoin de l'écrire vous-même. Si vous pouvez vous en tenir à IEnumerable, utiliser LINQ sera beaucoup plus simple.
Utilisez Enumerable.Concat () . Dans la source , vous pouvez voir que ConcatIterator renvoie d'abord tous les éléments de d'abord , puis de deuxième . Il ne copie pas les IEnumerables originaux (ou les tableaux dans ce cas), il utilise des références.
( REMARQUE: pour une vitesse maximale et de nombreux petits IEnumerables vous ne devriez pas faire cela, mais pour une consommation de mémoire minimale et quelques gros IEnumerables cela fonctionne )
Comme le mentionne InBetween, vous ne devriez vraiment pas créer une nouvelle liste. J'imagine que sa solution est quelle est la «meilleure» solution.
En termes de réponse à votre question initiale, vous allez avoir des problèmes en raison du fonctionnement de Garbage Collection avec .NET ( https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals ).
Afin de contourner ce problème, le meilleur moyen sera de ne pas utiliser l'un des conteneurs intégrés pour vous permettre d'avoir un contrôle total de votre utilisation de la mémoire sans utiliser aucune classe et allouer tout à la pile.
Voici quelques exemples de gestion des allocations pour avoir un contrôle plus étroit sur la mémoire en raison de la portée:
List<int> concat = new List<int>();
using (int[] a = Enumerable.Range(0, 1000 * 1024 * 1024 / 4).ToArray()){
concat.AddRange(a.Skip(500 * 1024 * 1024 / 4));
}
using (int[] b = Enumerable.Range(0, 1000 * 1024 * 1024 / 4).ToArray()){
concat.AddRange(b.Skip(500 * 1024 * 1024 / 4));
}
// Do a GC.Collect() if you really don't want to put this in it's own scope for some reason.
La section ci-dessus aura a et b éliminés immédiatement car ils sont des allocations de pile sans utiliser de classes. Cela permettra d'utiliser correctement la différence de nettoyage de la mémoire entre les structures et les classes.
Il existe une autre façon de le faire un peu plus lourdement.
void MyFunc(IList<int> combinedList)
{
int[] a = new int[LARGE_COUNT]; // This will initialize to the default value of the type. (default)int == 0
int[] b = new int[LARGE_COUNT];
// Add whatever you want to combinedList. This will just add both.
combinedList.AddRange(a);
combinedList.AddRange(b);
}
Le GC.Collect () est un moyen très agressif de se déplacer en apprenant la bonne façon dont le ramasse-miettes de .NET est correctement configuré pour fonctionner.
Vous tenez toujours les baies d'origine. Une fois que vous les avez ajoutés à la liste, effacez-les afin qu'ils puissent être marqués pour le garbage collection. Bien que la mémoire ne puisse pas être libérée tout de suite, le GC déterminera quand le faire.
Pour être honnête, vous ne savez pas ce que vous demandez. Après la concaténation,
a,betconcatsont indépendants - changer un élément deane changera pas < code> concat ou vice versa, par exemple. Cela signifie que ces éléments doivent être stockés indépendamment. Vous pouvez maintenant conserver unIEnumerable qui n'a pas cette propriété:IEnumerable concat = a.Concat (b); code >. Mais ce n'est pas une List . Si vous avez besoin d'uneList et de deux tableaux, vous ne pouvez pas les concaténer dans une sorte de "référencement". Si vous pouviez être plus précis sur la situation dans son ensemble, cela aiderait.Je ne peux pas effacer les tableaux d'origine. Ils sont persistants. J'ai juste besoin de les concaténer, de faire quelques recherches, puis de disposer la liste concaténée.
Alors en avez-vous réellement besoin sous forme de liste concaténée, ou simplement de séquence ? Vous pouvez rechercher facilement dans une séquence. C'est pourquoi les exigences exactes sont importantes.
Ah,
IEnumerable concat = a.Concat (b); fonctionne parfaitement. Bon à savoir.J'étais juste curieux de savoir comment prendre des tableaux / segments de tableau et parcourir le nouveau tableau concaténé. N'avait pas à être une liste.
Ce n'est pas un tableau concaténé - c'est l'astuce. C'est juste une séquence qui "sait" que c'est la concaténation de deux tableaux. Tout ce que
IEnumerable doit pouvoir faire est de renvoyer un itérateur sur la séquence. Je vais ajouter une réponse maintenant.Ce que je crois, c'est que si nous utilisons le pointeur de int [] a, nous pourrions atteindre cet objectif d'utilisation de moins de mémoire