【问题标题】:Word frequency program - file input too large?词频程序——文件输入太大?
【发布时间】:2017-10-10 21:52:25
【问题描述】:

我仍在努力解决这篇文章中提到的问题: Sorting vector of strings with leading numbers

原来的问题如下:

编写一个完整的 C++ 程序,输出文件 input.txt 中最常用的 k 个单词,按频率降序排列每行一个,其中 k 是从输入读取的非负整数。关系是任意打断的,如果 input.txt 中只有 u 个不同的词且 u

感谢那些建议使用结构的人,我最终得到了一个更高效、代码更少的解决方案。

然而,问题是,对于比较大的文本文件(超过 400000 个单词),我的程序可以运行 5 分钟以上并且没有任何结果。该程序在小文件输入上完美运行。我不确定是因为文件太大,还是算法本身有问题导致内存溢出/损坏。

这是我的程序代码:

struct word_freq {
int freq;
string word;
};

bool operator<(const word_freq& a, const word_freq& b) {
    return a.freq < b.freq;
}
void word_frequencies(ifstream& inf, int k)
{
vector <string> input;
string w;
while (inf >> w)
{
    remove_punc(w);
    input.push_back(w);
}
sort(input.begin(), input.end());

// initialize frequency vector
vector <int> freq;
for (size_t i = 0; i < input.size(); ++i) freq.push_back(1);

// count actual frequencies
int count = 0;
for (size_t i = 0; i < input.size()-1; ++i)
{
    if (input[i] == input[i+1])
    {
        ++count;
    } else
    {
        freq[i] += count;
        count = 0;
    }
}

// words+frequencies
vector <word_freq> wf;
for (int i = 0; i < freq.size(); ++i)
{
    if (freq[i] > 1 || is_unique(input, input[i]))
    {
        word_freq st = {freq[i], input[i]};
        wf.push_back(st);
    }
}

// printing
sort(wf.begin(), wf.end());
if (wf.size() < k)
{
    for (int i = wf.size()-1; i >= 0; --i)
    {
        cout << wf[i].word << " " << wf[i].freq << endl;
    }
} else
{
    for (int i = wf.size()-1; i >= wf.size()-1-k; --i)
    {
        cout << wf[i].word << " " << wf[i].freq << endl;
    }
}
}

如果有人能指出所犯的错误,将不胜感激。

【问题讨论】:

  • 调试器会告诉你到底发生了什么。让您的应用程序运行一会儿,然后暂停它以查看它当前在做什么。
  • 您是否尝试使用调试器单步执行您的代码?或者,当您认为程序停止时,使用调试器附加到您的程序进行调查?
  • 你说你不能使用除std::vectorstd::string 之外的算法或容器,但是你正在使用std::sort() 还有什么你没有告诉我们的?
  • valgrind 如果卡住了也很有用
  • 如果您希望人们帮助您,请先更好地帮助人们。您的代码难以阅读。

标签: c++ file-io text-files fstream word-frequency


【解决方案1】:

你让你的程序在内存和计算上做得太匹配了。首先,您将所有单词读入内存并对其进行排序。然后计算频率并填充另一个向量。您应该首先拥有std::vector&lt;word_freq&gt;,使其按单词排序(通过将元素插入适当的位置)并插入新元素或在现有元素上增加计数器。然后按频率处理这个向量并打印。

例如你可以如何重写你的循环:

struct word_freq {
    int freq;
    std::string word;

    word_freq( const std::string &w ) : word( w ), freq( 0 ) {}
};


void addWord( std::vector<word_freq> &v, const std::string &word )
{
     word_freq tmp( word );
     auto p = std::equal_range( v.begin(), v.end(), tmp, 
         []( const word_freq &w1, const word_freq &w2 ) {
             return w1.word < w2.word;
     } );
     if( p.first == p.second )  // not found
         p.first = v.insert( p.second, tmp ); // insert into proper place
     p.first->freq++; // increase freq counter
}

// ......
std::vector<word_freq> words;
string w;
while (inf >> w)
{
    remove_punc(w);
    addWord( words, w );
}
// here your vector sorted by words, there are no dups and counters have proper value already
// just resort it by freq and print

关于如何保持向量排序的详细信息可以在这里找到how do you insert the value in a sorted vector?

另一方面,保持std::vector&lt;word_freq&gt; 排序将需要将匹配插入到向量的中间或开头,这可能会非常昂贵且缓慢。因此,如果您实现所描述的逻辑并使其适用于小示例,并且对于您的大输入来说仍然太慢 - 您应该对索引向量而不是 word_freq 本身的向量进行排序。这仍然需要插入到整数向量的开头或中间,但是这样的操作要便宜得多,而且速度要快得多。可以在此处找到有关如何排序索引而不是向量本身的详细信息:compare function of sort in c++ for index sort

【讨论】:

    【解决方案2】:

    如果您在分配向量后使用reserve(int), 性能会好很多。

    推回向量会不断导致内存碎片。

    原因是向量不断超出其分配的边界,并且经常被重新分配。重新分配小对象通常很昂贵,并且会直接影响性能。

    最初使用足够大的内存块调用reserve,然后在向量的大小与其容量匹配时再次调用它,有助于避免此问题。

    更多:

    What is memory fragmentation?

    这里:

    Should I worry about memory fragmentation with std::vector?

    带有性能测量的小型演示:

    #include <chrono>
    #include <vector>
    #include <iostream>
    
    int main()
    {
            std::vector<std::string> slow;
            std::string d = "divide and conquer";
    
            std::chrono::time_point<std::chrono::system_clock> start, end;
            start = std::chrono::system_clock::now();
    
            // I get reallocated all the time
            for ( int i=0; i < 100000; i++ )
            {
                slow.push_back(d);
            }
    
            end = std::chrono::system_clock::now();
    
            std::chrono::duration<double> elapsed_seconds = end-start;
            std::time_t end_time = std::chrono::system_clock::to_time_t(end);
    
            std::cout << "elapsed time v1: " << elapsed_seconds.count() << "s\n";
    
            start = std::chrono::system_clock::now();
    
            //I don't move around
            slow.reserve(100000);
            slow.clear();
            for ( int i=0; i < 100000; i++ )
            {
                slow.push_back(d);
            }
    
            end = std::chrono::system_clock::now();
    
            elapsed_seconds = end-start;
            end_time = std::chrono::system_clock::to_time_t(end);
    
            std::cout << "elapsed time v2: " << elapsed_seconds.count() << "s\n";
            return 0;
    }
    

    输出:

        elapsed time v1: 0.014085s
    
        elapsed time v2: 0.004597s
    

    【讨论】:

    • 我没有投反对票,如果事先知道元素的数量,我也不反对调用reserve,但是一个保证连续内存的向量到底是如何导致内存碎片的呢?跨度>
    • @ChristopherPisz:点击提供的链接并找出答案
    • @ChristopherPisz:碎片不会发生,因为存储是连续的,它发生是因为你一直在移动扩展它,频繁推回并且没有保留(理论上)。 Piotr 的回答进一步链接到了对内存碎片如何发生的很好的解释。
    • @ChristopherPisz 简单示例:假设您的总内存为 10 个字节。您将在向量中一个一个地推送 8 个字节。如果您从使用保留开始,向量分配 8 个字节,一切正常。如果你 not 保留,向量开始分配,比如说,6 个字节。字节 # 1 到 6 推送很好,但字节 # 7 不适合。向量需要分配另一个可以容纳旧数据的块以及新的空间,,6+字节,因此它可以在释放旧的内存块之前将旧数据复制到新块。但是,总内存只剩下 4 个字节。
    • @ChristopherPisz 当然,“任何地方”的其他类型的分配也可能导致碎片,但这里的论点是扩展 std::vector 是否会导致碎片。您似乎还声称连续的内存分配不会导致碎片。这完全是错误的。也许你误会了,碎片化的不是连续的内存块本身,而是整个内存池。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-07
    • 2015-11-28
    • 1970-01-01
    • 2020-03-26
    • 1970-01-01
    • 2011-04-14
    相关资源
    最近更新 更多