2010-11-29 5 views
8

Comme je suis plutôt nouveau à linq, j'aimerais demander si ma compréhension est correcte dans l'exemple suivant. Supposons que j'ai une très grande collection de noms d'animaux (100k enregistrements), je voudrais les filer et traiter les éléments filtrés dans une méthode très longue (2 semaines). Les méthodes RunWithLinq() et RunWithoutLinq() font complètement la même chose. Est-ce vrai qu'en utilisant la première méthode, la collection originale (grande) restera en mémoire après avoir quitté la méthode, et ne sera pas touchée par GC, alors qu'en utilisant la méthode linq-less, la collection sera supprimée par GC?Question de mémoire Linq

Je vous serais reconnaissant pour une explication.

class AnimalProcessor 
{ 
    private IEnumerable<string> animalsToProcess; 
    internal AnimalProcessor(IEnumerable<string> animalsToProcess) 
    { 
     this.animalsToProcess = animalsToProcess; 
    } 
    internal void Start() 
    { 
     //do sth for 2 weeks with the collection 
    } 
} 
class Program 
{ 
    static void RunWithLinq() 
    { 
     var animals = new string[] { "cow", "rabbit", "newt", "ram" }; 
     var filtered = from animal in animals 
         where animal.StartsWith("ra") 
         select animal; 
     AnimalProcessor ap = new AnimalProcessor(filtered); 
     ap.Start(); 
    } 
    static void RunWithoutLinq() 
    { 
     var animals = new string[] { "cow", "rabbit", "newt", "ram" }; 
     var filtered = new List<string>(); 
     foreach (string animal in animals) 
      if(animal.StartsWith("ra")) filtered.Add(animal); 
     AnimalProcessor ap = new AnimalProcessor(filtered); 
     ap.Start(); 
    } 
} 
+0

Qu'est-ce qui vous donne cette impression en premier lieu? – Jay

+0

@Jay Lorsque j'ai utilisé le débogueur, il n'est pas entré dans la requête tant que le résultat n'a pas été utilisé, ce que je ne savais pas précédemment. – nan

+0

Les 'animaux' seront-ils collectés dans la méthode non-linq? Mai (peut-être naïf) la compréhension est que les variables locales sortent de la portée (et peuvent être collectées) après la parenthèse fermante de la fonction, c'est-à-dire après ap.Start() renvoie deux semaines plus tard (à moins qu'il ne soit réellement exécuté de manière asynchrone, dans le cas où ce commentaire est vide). =) – Jens

Répondre

7

Eh bien, animals sera admissible à la collecte à la fin de chaque méthode, donc strictement votre déclaration est fausse. animals devient éligible pour la collecte plus tôt dans le cas non-LINQ, donc l'essentiel de votre déclaration est vrai.

Il est vrai que l'utilisation de la mémoire de chacun diffère. Cependant, il y a une implication ici que LINQ est généralement pire en termes d'utilisation de la mémoire, alors qu'en réalité il permet très souvent une bien meilleure utilisation de la mémoire que l'autre type d'approche (bien qu'il existe des façons non-LINQ de la façon LINQ, j'étais très friand de la même approche de base à ce problème particulier quand j'ai utilisé .NET2.0).

Considérons les deux méthodes, non LINQ premier:

var animals = new string[] { "cow", "rabbit", "newt", "ram" }; 
var filtered = new List<string>(); 
foreach (string animal in animals) 
//at this point we have both animals and filtered in memory, filtered is growing. 
    if(animal.StartsWith("ra")) filtered.Add(animal); 
//at this point animals is no longer used. While still "in scope" to the source 
//code, it will be available to collection in the produced code. 
AnimalProcessor ap = new AnimalProcessor(filtered); 
//at this point we have filtered and ap in memory. 
ap.Start(); 
//at this point ap and filtered become eligible for collection. 

Il convient de noter deux choses. Un "admissible" à la collecte ne signifie pas que la collecte aura lieu à ce moment-là, mais seulement à tout moment dans le futur. Deuxièmement, la collecte peut se produire lorsqu'un objet est encore dans la portée s'il n'est pas réutilisé (et même dans certains cas où il est utilisé, mais c'est un autre niveau de détail). Les règles d'étendue se rapportent à la source du programme et sont une question de ce qui peut arriver lorsque le programme est écrit (le programmeur peut ajouter du code qui utilise l'objet), les règles d'éligibilité de la collection GC se rapportent au programme compilé. programme a été écrit (le programmeur aurait pu ajouter un tel code, mais ils ne l'ont pas fait).

Maintenant, regardons le cas LINQ:

var animals = new string[] { "cow", "rabbit", "newt", "ram" }; 
var filtered = from animal in animals 
       where animal.StartsWith("ra") 
       select animal; 
// at this pint we have both animals and filtered in memory. 
// filtered defined as a class that acts upon animals. 
AnimalProcessor ap = new AnimalProcessor(filtered); 
// at this point we have ap, filtered and animals in memory. 
ap.Start(); 
// at this point ap, filtered and animals become eligible for collection. 

Voici donc dans ce cas, aucun des objets pertinents peuvent être recueillis jusqu'à la fin.

Cependant, notez que filtered n'est jamais un objet volumineux. Dans le premier cas filtered est une liste qui contient quelque part dans la plage de 0 à n objets, où n est la taille de animals. Dans le second cas, filtered est un objet qui fonctionnera sur animals selon les besoins et a lui-même essentiellement de la mémoire constante.

Par conséquent, l'utilisation de mémoire maximale de la version non-LINQ est plus élevée, car il existera un point où animals existe toujours et filtered contient tous les objets pertinents. Comme la taille de animals augmente avec les modifications apportées au programme, c'est en fait la version non-LINQ qui risque le plus de rencontrer une pénurie de mémoire sérieuse, car l'état d'utilisation de la mémoire de pointe est pire dans le cas non-LINQ.

Une autre chose à considérer, c'est que dans un cas réel où nous avions assez d'éléments à se soucier de la consommation de mémoire, c'est comme si notre source ne va pas être une liste. Considérez:

IEnumerable<string> getAnimals(TextReader rdr) 
{ 
    using(rdr) 
    for(string line = rdr.ReadLine(); line != null; line = rdr.ReadLine()) 
     yield return line; 
} 

Ce code lit un fichier texte et renvoie chaque ligne à la fois. Si chaque ligne contenait le nom d'un animal, nous pourrions l'utiliser au lieu de var animals comme source pour filtered. Dans ce cas, la version LINQ utilise très peu de mémoire (ne nécessitant qu'un seul nom d'animal pour être en mémoire à la fois) tandis que la version non-LINQ utilise beaucoup plus de mémoire (chargement de chaque nom d'animal "ra" dans la mémoire avant d'autres actions). La version LINQ commencera également à être traitée au bout de quelques millisecondes au maximum, tandis que la version non-LINQ devra tout d'abord charger tout ce qui est nécessaire avant de pouvoir effectuer un seul travail. Par conséquent, la version LINQ pourrait heureusement gérer des gigaoctets de données sans utiliser plus de mémoire que pour une poignée, alors que la version non-LINQ aurait des problèmes de mémoire. Enfin, il est important de noter que cela n'a rien à voir avec LINQ lui-même, en ce qui concerne les différences entre l'approche que vous prenez avec LINQ et l'approche que vous prenez sans LINQ. Pour l'équivalent LINQ à l'utilisation non-LINQ:

var filtered = (from animal in animals 
        where animal.StartsWith("ra") 
        select animal).ToList(); 

Pour rendre l'équivalent non LINQ à l'LINQ utiliser

var filtered = FilterAnimals(animals); 

où vous définissez également:

private static IEnumerable<string> FilterAnimals(IEnumerable<string> animals) 
{ 
    foreach(string animal in animals) 
    if(animal.StartsWith("ra")) 
     yield return animal; 
} 

qui utilise les techniques .NET 2.0 mais vous pouvez faire de même avec .NET 1.1 (mais avec plus de code) en créant un objet dérivé de IEnumerable

+0

Merci pour une explication très complète, en particulier sur l'utilisation de pointe et la portée. – nan

+0

Si ce n'est pas complet, ce n'est pas aussi amusant d'écrire :) –

2

Oui, c'est vrai - parce que la variable filtered est essentiellement la requête , pas les résultats de la requête. Itérer sur elle réévaluera la requête à chaque fois.

Si vous voulez les faire la même chose, vous pouvez simplement appeler ToList:

var filtered = animals.Where(animal => animal.StartsWith("ra")) 
         .ToList(); 

(je l'ai converti d'une syntaxe d'expression de requête pour « notation point » parce que dans ce cas, il est plus simple de cette façon.

+0

Donc, la même chose serait si je au lieu d'interroger la méthode utilisée qui fait 'yield return' et renvoie IEnumberable? – nan

+0

@Andrzej: Oui, absolument. –

+0

@Andrzej, notez que l'utilisation de la mémoire crête est augmentée avec l'approche basée sur la liste (que ce soit LINQ ou non), car la taille de 'filtered' est plus grande avec cette approche. –

3

La méthode basée sur LINQ conserve la collection d'origine en mémoire, mais ne stocke pas de collection séparée avec les éléments filtrés.

Pour modifier ce comportement, appelez .ToList().