我正在尝试读取一个大文本文件,并在其中输出不同的单词以及计数。 到目前为止,我已经尝试了几次,这是迄今为止我提出的最快的解决方案。

private static readonly char[] separators = { ' ' };

public IDictionary<string, int> Parse(string path)
{
    var wordCount = new Dictionary<string, int>();

    using (var fileStream = File.Open(path, FileMode.Open, FileAccess.Read))
    using (var streamReader = new StreamReader(fileStream))
    {
        string line;
        while ((line = streamReader.ReadLine()) != null)
        {
            var words = line.Split(separators, StringSplitOptions.RemoveEmptyEntries);

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

    return wordCount;
}

我如何衡量我的解决方案

我有200MB的文本,我知道该文本的总字数(通过文本编辑器)。 我正在使用Stopwatch class ,对单词进行计数以确保准确性并计算花费的时间。 到目前为止,大约需要9秒钟。

其他尝试

  • 我试图利用多线程通过TPL库将工作拆分出来。 这涉及批处理多行,将批处理的处理发送到单独的任务,并在字典中锁定读/写操作。 但是,这似乎无法为我提供任何性能改进。
  • 花了大约30秒。 我怀疑要对字典进行读写操作的锁定成本太高,无法获得任何性能。
  • 我还查看了ConcurrentDictionary类型,但是根据我的理解, AddOrUpdate方法确实需要调用代码来处理同步,并且没有带来任何性能好处。

我敢肯定,有一个更快的方法可以实现这一目标! 是否有更好的数据结构可用于此问题?

欢迎对我的解决方案提出任何建议/批评-尝试在此处学习和改进!

干杯。

更新:这是我正在使用的测试文件的链接

===============>>#1 票数:12 已采纳

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


我分三步设法节省了约43%的总运行时间。

首先,我测量了您的代码并得到了:

原始代码测量

这似乎表明这里有两个热点我们可以尝试解决:

  1. 字符串拆分(SplitInternal)
  2. 字典维护(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;

这取决于以下事实:

  1. TryGetValue比检查单词是否存在然后检索其当前计数便宜
  2. 如果TryGetValue无法获取值(键不存在),则它将在此处将currentCount变量初始化为其默认值0。这意味着我们实际上不需要检查该单词是否确实存在。
  3. 我们可以通过索引器向字典添加新单词(它将覆盖现有值或向字典添加新的键值)

因此,最终循环如下所示:

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

新的度量显示:

新测量

细节:

  1. 我们从6876ms变为5013ms
  2. 我们浪费了花在SplitInternalFindEntryget_Item
  3. 我们花了一些时间在TryGetValueSubstring

以下是差异的详细信息:

区别

如您所见,我们损失的时间超过了获得新时间的时间,从而带来了净改进。

但是,我们可以做得更好。 我们在这里进行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;
}

===============>>#2 票数:6

您的方法似乎符合大多数人的处理方式。 您应该正确地注意到,使用多线程并没有带来任何明显的好处,因为瓶颈很可能是IO约束,并且无论您拥有哪种硬件,您读取的速度都不会比硬件支持的速度快。

如果您确实在寻求速度上的提高(我怀疑您会获得任何提高),则可以尝试实现生产者-消费者模式,其中一个线程读取文件,其他线程处理行(也许然后并行检查单词中的单词)线)。 这里需要权衡的是,您要添加很多复杂的代码以换取微不足道的改进(只有基准测试才能确定这一点)。

http://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem

编辑:也看看ConcurrentDictionary

===============>>#3 票数:6

只需更改一下,我就获得了很多好处(从200秒的文件中从25秒增加到20秒):

int cnt;

if (wordCount.TryGetValue(word, out cnt))
{
    wordCount[word] = cnt + 1;
}
else
....

基于ConcurrentDictionary<>Parallel.ForEach变体(使用IEnumerable<>重载)。 请注意,不是使用int ,而是使用InterlockedInt ,该InterlockedInt使用Interlocked.Increment来递增自身。 作为引用类型,它可以与ConcurrentDictionary<>.GetOrAdd ...一起正常使用ConcurrentDictionary<>.GetOrAdd

public class InterlockedInt
{
    private int cnt;

    public int Cnt
    {
        get
        {
            return cnt;
        }
    }

    public void Increment()
    {
        Interlocked.Increment(ref cnt);
    }
}

public static IDictionary<string, InterlockedInt> Parse(string path)
{
    var wordCount = new ConcurrentDictionary<string, InterlockedInt>();

    Action<string> action = line2 =>
    {
        var words = line2.Split(separators, StringSplitOptions.RemoveEmptyEntries);

        foreach (var word in words)
        {
            wordCount.GetOrAdd(word, x => new InterlockedInt()).Increment();
        }
    };

    IEnumerable<string> lines = File.ReadLines(path);
    Parallel.ForEach(lines, action);

    return wordCount;
}

请注意,使用Parallel.ForEach的效率要比直接为每个物理核心使用一个线程低(您可以在历史记录中看到效果)。 虽然这两种解决方案在我的PC上花费的挂墙时钟都不到10秒,但Parallel.ForEach占用的CPU时间为55秒,而Thread解决方案的时间为33秒。

还有另一种技巧,其价值约为5-10%:

public static IEnumerable<T[]> ToBlock<T>(IEnumerable<T> source, int num)
{
    var array = new T[num];
    int cnt = 0;

    foreach (T row in source)
    {
        array[cnt] = row;
        cnt++;

        if (cnt == num)
        {
            yield return array;
            array = new T[num];
            cnt = 0;
        }
    }

    if (cnt != 0)
    {
        Array.Resize(ref array, cnt);
        yield return array;
    }
}

您将数据包中的行“分组”(选择10到100之间的数字),以便减少线程内通信。 然后,工作人员必须对收到的行进行一次foreach

===============>>#4 票数:2

使用200mb的文本文件,以下内容在我的计算机上花费了不到5秒的时间。

    class Program
{
    private static readonly char[] separators = { ' ' };
    private static List<string> lines;
    private static ConcurrentDictionary<string, int> freqeuncyDictionary;

    static void Main(string[] args)
    {
        var stopwatch = new System.Diagnostics.Stopwatch();
        stopwatch.Start();

        string path = @"C:\Users\James\Desktop\New Text Document.txt";
        lines = ReadLines(path);
        ConcurrentDictionary<string, int> test = GetFrequencyFromLines(lines);

        stopwatch.Stop();
        Console.WriteLine(@"Complete after: " + stopwatch.Elapsed.TotalSeconds);
    }

    private static List<string> ReadLines(string path)
    {
        lines = new List<string>();
        using (var fileStream = File.Open(path, FileMode.Open, FileAccess.Read))
        {
            using (var streamReader = new StreamReader(fileStream))
            {
                string line;
                while ((line = streamReader.ReadLine()) != null)
                {
                    lines.Add(line);
                }
            }
        }
        return lines;            
    }

    public static ConcurrentDictionary<string, int> GetFrequencyFromLines(List<string> lines)
    {
        freqeuncyDictionary = new ConcurrentDictionary<string, int>();
        Parallel.ForEach(lines, line =>
        {
            var words = line.Split(separators, StringSplitOptions.RemoveEmptyEntries);

            foreach (var word in words)
            {
                if (freqeuncyDictionary.ContainsKey(word))
                {
                    freqeuncyDictionary[word] = freqeuncyDictionary[word] + 1;
                }
                else
                {
                    freqeuncyDictionary.AddOrUpdate(word, 1, (key, oldValue) => oldValue + 1);
                }
            }
        });

        return freqeuncyDictionary;
    }
}

===============>>#5 票数:1

如果您要计算一个特定的单词,可以使用此处链接的strtok函数,并将每个单词与您要评估的单词进行比较,我认为这不是很昂贵,但是我从来没有尝试过使用大文件夹。 。

===============>>#6 票数:1

我建议将流缓冲区的大小设置为更大并匹配:

    using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 8192))
    using (var streamReader = new StreamReader(fileStream, Encoding.UTF8, false, 8192))

首先,您的代码导致缓冲区太小,无法进行此类工作。 其次,由于读取器的缓冲区小于流的缓冲区,因此首先将数据复制到流的缓冲区,然后再复制到读取器的缓冲区。 对于您正在做的工作,这可能是性能的破坏者。

当缓冲区大小匹配时,将永远不会使用流的缓冲区-实际上,永远不会分配它。

  ask by Prabu translate from so

未解决问题?本站智能推荐:

4回复

计算巨大文本文件的词频[重复]

这个问题在这里已有答案: 解析1 TB的文本并有效地计算每个单词的出现次数 16个答案 我有一个巨大的文本文件(大于可用的RAM内存)。 我需要计算所有单词的频率,并将单词和频率计数输出到一个新文件中。 结果应按频率计数的降序排序。 我的方法:
3回复

如何循环并比较两个文本文件中的数百万个值?

我有两个文本文件文件(TXT),其中包含超过200万个不同的文件名。 我想遍历第一个文件中的所有名称,并找到第二个文本文件中也存在的名称。 我试过循环StreamReader但需要花费很多时间。 我也尝试了下面的代码,但它仍然需要太多时间。 有什么比较文件的好方法?
2回复

将Trie编码到文件中以避免重建

我用一个约有18万个单词的字典构造了一个约有40万个节点的Trie。 问题在于,在您的手机上构建该树需要花费太长时间。 因此,我决定只创建一次特里树,并以某种格式将其存储到磁盘上,以便在需要时进行快速娱乐。 但是我无法提出一种好的格式来存储它。 什么是最有效的编码trie的格式,
4回复

文本自动完成的最佳数据结构是什么?

我有很长的单词列表,我想显示以用户输入的文本开头的单词。 当用户输入字符时,应用程序应更新显示给用户的列表。 它应该像Android上的AutoCompleteTextView。 我只是想知道存储单词的最佳数据结构,以便搜索速度非常快。
3回复

以O(1)时间检索堆栈中的Min元素

我问这个问题的原因是因为我无法理解为什么我认为的方式无法应用于这个特定问题 " ” 我的基本解决方案:如果堆栈类中没有变量,那么每当我们将一个项目推入堆栈时,我们都会检查它是否小于 min变量。 如果是,则将值分配给最小值,如果不忽略则将其赋值。 您仍然会像最小函数那样获得
2回复

矩阵中给出的区域的中位数

给定矩阵(nxn)为1和0,其中1代表土地,0代表水。 如何以最有效的方式找到土地面积的中位数? 例如: 1 1 0 0 0 1 0 0 1 1 1 0 1 0 0 有三个岛屿,它们的面积[1,2,4],中位数是2 岛可以由包含1的连续非对角线单元组成:例如
3回复

快速数据结构或算法,以查找图像堆栈中每个像素的平均值

我有一堆图像,我想在其中计算堆栈中每个像素的平均值。 例如,let (x_n,y_n)是第n个图像中的(x,y)像素。 因此,图像堆栈中三个图像的像素(x,y)的平均值是: 我的第一个想法是将每个图像的所有像素强度加载到具有单个线性缓冲区的数据结构中,如下所示: 为了找到
5回复

为什么我们使用链表来解决哈希表中的冲突?

我想知道为什么许多语言(Java,C ++,Python,Perl等)使用链表实现哈希表以避免冲突而不是数组? 我的意思是代替链表的桶,我们应该使用数组。 如果问题是关于数组的大小那么这意味着我们有太多的冲突,所以我们已经有哈希函数的问题,而不是我们解决冲突的方式。 我误会了什么吗?
4回复

用于在C ++中快速搜索的数据结构

我需要在数据结构中存储如下的值, 现在我需要匹配x,y,z double值并获得相应的id(int)值。 我可能需要存储大约400000个值。 我应该使用哪些数据结构进行有效搜索? C ++是否附带任何支持我的要求的内置结构。
2回复

现实生活中的人排队数据结构

您将如何为现实生活中的人排队建模? 考虑以下主要限制条件:-先进先出-任何时候随机元素都可以离开队列-pop应该始终将一个元素返回到队列中-队列中的任何元素都是可以唯一标识的(例如,社会保险号) 我想出的最好的解决方案是既维护fifo约束的队列,又维护用于管理离开人员的哈希集。 当