我正在尝试通过在 C# 中实现 Peter Norvig 的拼写校正器来更多地了解 LINQ。
第一部分涉及获取一个大的单词文件(大约 100 万个)并将其放入字典中,其中
key
是单词,value
是出现的次数。
我通常会这样做:
foreach (var word in allWords)
{
if (wordCount.ContainsKey(word))
wordCount[word]++;
else
wordCount.Add(word, 1);
}
其中
allWords
是 IEnumerable<string>
在 LINQ 中我目前正在这样做:
var wordCountLINQ = (from word in allWordsLINQ
group word by word
into groups
select groups).ToDictionary(g => g.Key, g => g.Count());
我通过查看所有
<key, value>
来比较这两个词典,它们是相同的,因此它们产生相同的结果。
foreach
循环需要3.82秒,LINQ查询需要4.49秒
我使用 Stopwatch 类对其进行计时,并且在 RELEASE 模式下运行。我不认为性能很差,我只是想知道是否有差异的原因。
我是否以低效的方式执行 LINQ 查询,或者我是否遗漏了某些内容?
这是完整的基准代码示例:
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);
}
LINQ 版本较慢的原因之一是创建了两个字典,而不是一个字典:
(内部)来自运营商分组; group by 还存储每个单独的单词。您可以通过查看 ToArray() 而不是 Count() 来验证这一点。这是您实际不需要的大量开销。
ToDictionary 方法基本上是实际 LINQ 查询上的 foreach,其中查询结果被添加到新字典中。根据唯一单词的数量,这也可能需要一些时间。
LINQ 查询速度稍慢的另一个原因是 LINQ 依赖于 lambda 表达式(Dathan 答案中的委托),并且与内联代码相比,调用委托会增加少量开销。
编辑:请注意,对于某些 LINQ 场景(例如 LINQ to SQL,但不是像这里这样的内存中 LINQ),重写查询会产生更优化的计划:
from word in allWordsLINQ
group word by word into groups
select new { Word = groups.Key, Count = groups.Count() }
但请注意,这不会为您提供字典,而是为您提供单词序列及其计数。您可以将其转换为字典
(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);
当我构建第二个示例,然后在 Reflector 的反汇编视图中打开它时,我得到以下信息:
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>();
});
可能需要更长的时间,因为发生了更多的函数调用,并且在一百万次迭代的过程中加起来。
通过完全滥用 LINQ,我能够使其与 foreach 循环大致相同,并且通常比 foreach 循环稍快,即使使用委托调用也是如此:
var wordCountLINQ = allWordsLINQ.Aggregate(new Dictionary<string, int>(), (wcld, w) => { wcld[w] = (wcld.ContainsKey(w) ? wcld[w] : 0) + 1; return wcld; })
即使更改
foreach
以使用类似的集合表达式也没有使其更快。
您可以使用 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);