2010-01-22 13 views
8

Estoy tratando de aprender un poco más sobre LINQ implementando el spelling corrector de Peter Norvig en C#.Método LINQ para agregar elementos a un diccionario

La primera parte consiste en tomar una gran file of words (alrededor de 1 millón) y ponerlo en un diccionario en el que el key es la palabra y la value es el número de ocurrencias.

Me suelo hacer esto de esta manera:

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

Dónde allWords es un IEnumerable<string>

En LINQ actualmente estoy haciendo de esta manera:

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

comparo la 2 diccionarios mirando todos los <key, value> y son idénticos, por lo que están produciendo los mismos resultados.

El bucle foreach toma 3,82 segundos y la consulta LINQ toma 4.49 segundos

estoy de sincronización utilizando la clase Cronómetro y estoy funcionando en modo de lanzamiento. No creo que el rendimiento sea malo. Me preguntaba si había alguna razón para la diferencia.

¿Estoy haciendo la consulta LINQ de forma ineficiente o me falta algo?

Actualización: aquí está el ejemplo de código de referencia completo:

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

No es posible comentar la diferencia a menos que publique el código de referencia. – JaredPar

+0

Acabo de agregar eso a la pregunta por ti. –

Respuesta

5

Una de las razones de la versión de LINQ es más lento, es porque en lugar de un diccionario, se crean dos diccionarios:

  1. (internamente) del grupo por el operador; el grupo también almacena cada palabra individual. Puede verificar esto mirando ToArray() en lugar de Count(). Esto es una gran cantidad de gastos generales que en realidad no necesita en su caso.

  2. El método ToDictionary es básicamente un foreach sobre la consulta LINQ real, donde los resultados de la consulta se agregan a un nuevo diccionario. Dependiendo de la cantidad de palabras únicas, esto también puede llevar algo de tiempo.

Otra razón por la que la consulta LINQ es un poco más lento, es porque se basa en LINQ expresiones lambda (el delegado en la respuesta de Datán), y llamar a un delegado añade una pequeña cantidad de sobrecarga en comparación con el código en línea.

Editar: Tenga en cuenta que para algunos escenarios de LINQ (como LINQ a SQL, pero no en memoria LINQ como aquí), la reescritura de la consulta genera un plan más optimizado:

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

Nota sin embargo , que esto no te da un diccionario, sino una secuencia de palabras y sus recuentos. Puede transformar esto en un diccionario con

(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

¿Puedo modificar la consulta LINQ para superar estos problemas y obtener el mismo resultado? –

+0

Por lo que sé, no en 3.5 o 4.0, no. Para que esto funcione, los operadores ToDictionary y GroupBy necesitarían cooperar cuando solo agregue datos. Para LINQ en memoria eso no sucede. – Ruben

1

Cuando construyo el segundo ejemplo y luego abrirlo en la vista desmontaje del reflector, me sale el siguiente:

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>(); 
}); 

Probablemente toma más tiempo solo porque hay más llamadas de función y en el transcurso de un millón de iteraciones que se suman.

+0

Eso tiene sentido, ¿hay una forma más "directa" de hacerlo en LINQ? –

+0

No realmente, que yo sepa. ¿Tal vez por una expresión de selección diferente? Estoy fuera de mi ámbito de experiencia tan pronto como el grupo está involucrado en la expresión. – Dathan

0

se puede solucionar el problema con la expresión 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 nunca solicitó ninguna solución en esa área. Esto solo repite el código de trabajo sin ningún problema de la pregunta. –

+0

No hice ninguna pregunta aquí, es una solución para la pregunta anterior. –

+0

Entonces, ¿qué parte de la pregunta responde? –

0

Por abusando completamente LINQ que era capaz de conseguir que sea alrededor de la misma y, a menudo ligeramente más rápido que el bucle foreach, incluso con una llamada delegado:

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

incluso cambiar el foreach utilizar un conjunto similar de expresión no hacerlo más rápido .

Cuestiones relacionadas