2010-01-22 2 views
8

J'essaie d'en savoir un peu plus sur LINQ en implémentant le spelling corrector de Peter Norvig en C#.Méthode LINQ pour ajouter des éléments à un dictionnaire

La première partie consiste à prendre un grand file of words (environ 1 million) et le mettre dans un dictionnaire où le key est le mot et le value est le nombre d'occurrences.

je le fais normalement comme ceci:

foreach (var word in allWords)              
{   
    if (wordCount.ContainsKey(word)) 
     wordCount[word]++; 
    else 
     wordCount.Add(word, 1); 
} 

allWords est un IEnumerable<string>

Dans LINQ Je suis actuellement le faire comme ceci:

var wordCountLINQ = (from word in allWordsLINQ 
         group word by word 
         into groups 
         select groups).ToDictionary(g => g.Key, g => g.Count()); 

Je compare la 2 dictionnaires en regardant tous les <key, value> et ils sont identiques, donc ils produisent les mêmes résultats.

La boucle foreach prend 3,82 secondes et la requête LINQ prend 4,49 secondes

Je synchronisation à l'aide de la classe Chronomètre et je suis en cours d'exécution en mode RELEASE. Je ne pense pas que la performance soit mauvaise. Je me demandais juste s'il y avait une raison pour la différence. Est-ce que je fais la requête LINQ d'une manière inefficace ou est-ce qu'il me manque quelque chose?

Mise à jour: est ici l'échantillon complet de code de référence:

public static void TestCode() 
{ 
    //File can be downloaded from http://norvig.com/big.txt and consists of about a million words. 
    const string fileName = @"path_to_file"; 
    var allWords = from Match m in Regex.Matches(File.ReadAllText(fileName).ToLower(), "[a-z]+", RegexOptions.Compiled) 
        select m.Value; 

    var wordCount = new Dictionary<string, int>(); 
    var timer = new Stopwatch();    
    timer.Start(); 
    foreach (var word in allWords)              
    {   
     if (wordCount.ContainsKey(word)) 
      wordCount[word]++; 
     else 
      wordCount.Add(word, 1); 
    } 
    timer.Stop(); 

    Console.WriteLine("foreach loop took {0:0.00} ms ({1:0.00} secs)\n", 
      timer.ElapsedMilliseconds, timer.ElapsedMilliseconds/1000.0); 

    //Make LINQ use a different Enumerable (with the exactly the same values), 
    //if you don't it suddenly becomes way faster, which I assmume is a caching thing?? 
    var allWordsLINQ = from Match m in Regex.Matches(File.ReadAllText(fileName).ToLower(), "[a-z]+", RegexOptions.Compiled) 
        select m.Value; 

    timer.Reset(); 
    timer.Start(); 
    var wordCountLINQ = (from word in allWordsLINQ 
          group word by word 
          into groups 
          select groups).ToDictionary(g => g.Key, g => g.Count()); 
    timer.Stop(); 

    Console.WriteLine("LINQ took {0:0.00} ms ({1:0.00} secs)\n", 
      timer.ElapsedMilliseconds, timer.ElapsedMilliseconds/1000.0);      
} 
+1

Il n'est pas possible de commenter la différence sauf si vous publiez le code de référence. – JaredPar

+0

Je viens d'ajouter cela à la question pour vous. –

Répondre

5

Une des raisons pour lesquelles la version LINQ est plus lent, est parce qu'au lieu d'un dictionnaire, deux dictionnaires sont créés:

  1. (interne) du groupe par l'opérateur; le groupe enregistre également chaque mot individuel. Vous pouvez vérifier ceci en regardant un ToArray() plutôt qu'un Count(). C'est beaucoup de frais généraux dont vous n'avez pas besoin dans votre cas. La méthode ToDictionary est fondamentalement un foreach sur la requête LINQ réelle, où les résultats de la requête sont ajoutés à un nouveau dictionnaire. En fonction du nombre de mots uniques, cela peut prendre du temps.

Une autre raison pour laquelle la requête LINQ est un peu plus lent, est parce que LINQ repose sur des expressions lambda (le délégué dans la réponse de Dathan), et d'appeler un délégué ajoute une quantité infime des frais généraux par rapport au code en ligne.

Edit: Notez que pour certains scénarios LINQ (tels que LINQ à LINQ SQL, mais pas en mémoire comme ici), la réécriture de la requête produit un plan plus optimisé:

from word in allWordsLINQ 
group word by word into groups 
select new { Word = groups.Key, Count = groups.Count() } 

Notez cependant , que cela ne vous donne pas un dictionnaire, mais plutôt une séquence de mots et leurs comptes. Vous pouvez le transformer en un dictionnaire avec

(from word in allWordsLINQ 
group word by word into groups 
select new { Word = groups.Key, Count = groups.Count() }) 
.ToDictionary(g => g.Word, g => g.Count); 
+0

Puis-je modifier la requête LINQ pour surmonter ces problèmes et obtenir le même résultat? –

+0

Pour autant que je sache, pas dans 3.5 ou 4.0, non. Pour que cela fonctionne, les opérateurs ToDictionary et GroupBy doivent coopérer lorsque vous ne faites qu'agréger des données. Pour LINQ en mémoire qui ne se produit pas. – Ruben

1

Lorsque je construis votre deuxième exemple, puis l'ouvrir en mode démontage de réflecteur, j'obtiens ce qui suit:

Dictionary<string, int> wordCountLINQ = allWordsLINQ.GroupBy<string, string>(delegate (string word) { 
    return word; 
}).Select<IGrouping<string, string>, IGrouping<string, string>>(delegate (IGrouping<string, string> groups) { 
    return groups; 
}).ToDictionary<IGrouping<string, string>, string, int>(delegate (IGrouping<string, string> g) { 
    return g.Key; 
}, delegate (IGrouping<string, string> g) { 
    return g.Count<string>(); 
}); 

Probablement cela prend plus de temps parce qu'il y a plus d'appels de fonction qui se produisent, et au cours d'un million d'itérations qui s'ajoutent.

+0

Cela a du sens, y a-t-il un moyen plus "direct" de le faire dans LINQ? –

+0

Pas vraiment, que je sache. Peut-être par une expression choisie différente? Je suis hors de mon domaine d'expérience dès que le groupe est impliqué dans l'expression. – Dathan

0

Vous pouvez résoudre votre problème en utilisant l'expression lambda:

var words = unitOfWork.DepartmentRepository.Get() 
      .GroupBy(a=>a.word).Select(s => new 
      { 
      Word = s.Key, 
      Count = s.Count() 
      }).ToDictionary(d=>d.Word, d=>d.Count); 
+0

OP n'a jamais demandé de solution dans ce domaine. Cela ne fait que répéter le code de travail sans aucun problème de la question. –

+0

Je n'ai posé aucune question ici, c'est une solution pour la question ci-dessus. –

+0

Alors, quelle partie de la question répond-elle? –

0

Par complètement abusait LINQ j'ai pu obtenir d'être autour de la même et souvent un peu plus rapide que la boucle foreach, même avec un appel délégué:

var wordCountLINQ = allWordsLINQ.Aggregate(new Dictionary<string, int>(), (wcld, w) => { wcld[w] = (wcld.ContainsKey(w) ? wcld[w] : 0) + 1; return wcld; }) 

même changer le foreach d'utiliser une expression ensemble similaire ne le rendre plus rapide .

Questions connexes