我能给出的最好的简短答案是测量,测量,测量。 Stopwatch
可以很好地了解花在哪里的时间,但是最终您最终会在上面花很多代码,否则您将不得不为此目的找到更好的工具。 我建议为此使用专用的探查器工具,其中有许多可用于C#和.NET的工具。
我分三步设法节省了约43%的总运行时间。
首先,我测量了您的代码并得到了:

这似乎表明这里有两个热点我们可以尝试解决:
- 字符串拆分(SplitInternal)
- 字典维护(FindEntry,Insert,get_Item)
花的最后一部分时间是在读取文件时,我真的怀疑通过更改代码的这一部分能否获得很多收益。 这里的另一个答案提到使用特定的缓冲区大小,我尝试了这一点,但无法获得可测量的差异。
第一个是字符串拆分,虽然有点容易,但需要重写一个非常简单的string.Split
调用, string.Split
拆分为更多代码。 我将处理一行的循环重写为:
while ((line = streamReader.ReadLine()) != null)
{
int lastPos = 0;
for (int index = 0; index <= line.Length; index++)
{
if (index == line.Length || line[index] == ' ')
{
if (lastPos < index)
{
string word = line.Substring(lastPos, index - lastPos);
// process word here
}
lastPos = index + 1;
}
}
}
然后,我将一个单词的处理重写为此:
int currentCount;
wordCount.TryGetValue(word, out currentCount);
wordCount[word] = currentCount + 1;
这取决于以下事实:
-
TryGetValue
比检查单词是否存在然后检索其当前计数便宜
- 如果
TryGetValue
无法获取值(键不存在),则它将在此处将currentCount
变量初始化为其默认值0。这意味着我们实际上不需要检查该单词是否确实存在。
- 我们可以通过索引器向字典添加新单词(它将覆盖现有值或向字典添加新的键值)
因此,最终循环如下所示:
while ((line = streamReader.ReadLine()) != null)
{
int lastPos = 0;
for (int index = 0; index <= line.Length; index++)
{
if (index == line.Length || line[index] == ' ')
{
if (lastPos < index)
{
string word = line.Substring(lastPos, index - lastPos);
int currentCount;
wordCount.TryGetValue(word, out currentCount);
wordCount[word] = currentCount + 1;
}
lastPos = index + 1;
}
}
}
新的度量显示:

细节:
- 我们从6876ms变为5013ms
- 我们浪费了花在
SplitInternal
, FindEntry
和get_Item
- 我们花了一些时间在
TryGetValue
和Substring
以下是差异的详细信息:

如您所见,我们损失的时间超过了获得新时间的时间,从而带来了净改进。
但是,我们可以做得更好。 我们在这里进行2次字典查找,其中涉及计算单词的哈希码,并将其与字典中的键进行比较。 第一个查询是TryGetValue
的一部分,第二个查询是wordCount[word] = ...
。
我们可以通过在字典内创建更智能的数据结构来删除第二个字典查找,但要消耗更多的堆内存。
我们可以使用Xanatos的把计数存储在对象中的技巧,以便删除第二个字典查找:
public class WordCount
{
public int Count;
}
...
var wordCount = new Dictionary<string, WordCount>();
...
string word = line.Substring(lastPos, index - lastPos);
WordCount currentCount;
if (!wordCount.TryGetValue(word, out currentCount))
wordCount[word] = currentCount = new WordCount();
currentCount.Count++;
这只会从字典中检索计数,另外1次额外出现不涉及字典。 方法的结果也将更改为以字典的一部分而不是int
形式返回此WordCount
类型。
最终结果:节省了约43%。

最后一段代码:
public class WordCount
{
public int Count;
}
public static IDictionary<string, WordCount> Parse(string path)
{
var wordCount = new Dictionary<string, WordCount>();
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 65536))
using (var streamReader = new StreamReader(fileStream, Encoding.Default, false, 65536))
{
string line;
while ((line = streamReader.ReadLine()) != null)
{
int lastPos = 0;
for (int index = 0; index <= line.Length; index++)
{
if (index == line.Length || line[index] == ' ')
{
if (lastPos < index)
{
string word = line.Substring(lastPos, index - lastPos);
WordCount currentCount;
if (!wordCount.TryGetValue(word, out currentCount))
wordCount[word] = currentCount = new WordCount();
currentCount.Count++;
}
lastPos = index + 1;
}
}
}
}
return wordCount;
}