如何加快计算大文件中单词出现的次数?

时间:2014-12-29 23:58:52

标签: c++ performance optimization file-io

我需要计算104gb文件中字符串"<page>"的出现次数,以获取给定Wikipedia转储中的文章数量。首先,我试过这个。

grep -F '<page>' enwiki-20141208-pages-meta-current.xml | uniq -c

但是,grep会在一段时间后崩溃。因此,我写了以下程序。但是,它仅在我的机器上处理20mb / s的输入文件,这是我的HDD的大约5%的工作负载。我怎样才能加快这段代码的速度?

#include <iostream>
#include <fstream>
#include <string>

int main()
{
    // Open up file
    std::ifstream in("enwiki-20141208-pages-meta-current.xml");
    if (!in.is_open()) {
        std::cout << "Could not open file." << std::endl;
        return 0;
    }
    // Statistics counters
    size_t chars = 0, pages = 0;
    // Token to look for
    const std::string token = "<page>";
    size_t token_length = token.length();
    // Read one char at a time
    size_t matching = 0;
    while (in.good()) {
        // Read one char at a time
        char current;
        in.read(&current, 1);
        if (in.eof())
            break;
        chars++;
        // Continue matching the token
        if (current == token[matching]) {
            matching++;
            // Reached full token
            if (matching == token_length) {
                pages++;
                matching = 0;
                // Print progress
                if (pages % 1000 == 0) {
                    std::cout << pages << " pages, ";
                    std::cout << (chars / 1024 / 1024) << " mb" << std::endl;
                }
            }
        }
        // Start over again
        else {
            matching = 0;
        }
    }
    // Print result
    std::cout << "Overall pages: " << pages << std::endl;
    // Cleanup
    in.close();
    return 0;
}

2 个答案:

答案 0 :(得分:1)

假设文件中没有使用类似

的大行
for (std::string line; std::getline(in, line); } {
    // find the number of "<page>" strings in line
}

必然会更快 !将每个字符作为一个字符的字符串读取是您可能做的最糟糕的事情。变得越来越慢很难。对于每个角色,流将执行以下操作:

  1. 检查是否有需要刷新的tie() ed流(没有,即没有意义)。
  2. 检查流是否状态良好(除非已到达终点,但此检查不能完全省略)。
  3. 在流的流缓冲区上调用xsgetn()
  4. 此函数首先检查缓冲区中是否有另一个字符(类似于eof检查但不同;在任何情况下,只有在缓冲区为空后才进行eof检查会删除大量的eof检查)
  5. 将字符传输到读取缓冲区。
  6. 让流检查是否达到所有(1)个字符并根据需要设置流标记。
  7. 那里有很多浪费!

    我无法想象为什么grep会失败,除非某条线大量超过预期的最大线长。尽管使用std::getline()std::string()可能会有更大的上限,但处理大线仍然无效。如果文件可能包含大量的行,那么使用以下内容可能更合理:

    for (std::istreambuf_iterator<char> it(in), end;
         (it = std::find(it, end, '<') != end; ) {
        // match "<page>" at the start of of the sequence [it, end)
    }
    

    对于仍然做得太多的流的糟糕实现。好的实现将非常有效地调用std::find(...),并且可能会在一个处检查多个字符,仅为每个第16次循环迭代添加一个检查和循环。我希望上面的代码将您的CPU绑定实现转变为I / O绑定实现。糟糕的实现可能仍然受CPU限制,但它应该仍然要好得多。

    无论如何,请记住启用优化!

答案 1 :(得分:1)

我使用此文件进行测试:http://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-meta-current1.xml-p000000010p000010000.bz2

使用您的代码需要大约2.4秒而不是11.5秒。由于不计算换行符,总字符数略有不同,但我认为这是可以接受的,因为它仅用于显示进度。

void parseByLine()
{
    // Open up file
    std::ifstream in("enwiki-latest-pages-meta-current1.xml-p000000010p000010000");
    if(!in)
    {
        std::cout << "Could not open file." << std::endl;
        return;
    }
    size_t chars = 0;
    size_t pages = 0;
    const std::string token = "<page>";

    std::string line;
    while(std::getline(in, line))
    {
        chars += line.size();
        size_t pos = 0;
        for(;;)
        {
            pos = line.find(token, pos);
            if(pos == std::string::npos)
            {
                break;
            }
            pos += token.size();
            if(++pages % 1000 == 0)
            {
                std::cout << pages << " pages, ";
                std::cout << (chars / 1024 / 1024) << " mb" << std::endl;
            }
        }
    }
    // Print result
    std::cout << "Overall pages: " << pages << std::endl;
}

这是一个将每行添加到缓冲区然后在达到阈值时处理缓冲区的示例。与第一个版本相比,它需要2秒而不是2.4。我使用了几个不同的缓冲区大小的阈值,并且在固定数量(16,32,64,4096)的线之后进行处理,只要有一些批处理,它们似乎大致相同。感谢Dietmar的想法。

int processBuffer(const std::string& buffer)
{
    static const std::string token = "<page>";

    int pages = 0;
    size_t pos = 0;
    for(;;)
    {
        pos = buffer.find(token, pos);
        if(pos == std::string::npos)
        {
            break;
        }
        pos += token.size();
        ++pages;
    }
    return pages;
}

void parseByMB()
{
    // Open up file
    std::ifstream in("enwiki-latest-pages-meta-current1.xml-p000000010p000010000");
    if(!in)
    {
        std::cout << "Could not open file." << std::endl;
        return;
    }
    const size_t BUFFER_THRESHOLD = 16 * 1024 * 1024;
    std::string buffer;
    buffer.reserve(BUFFER_THRESHOLD);

    size_t pages = 0;
    size_t chars = 0;
    size_t progressCount = 0;

    std::string line;
    while(std::getline(in, line))
    {
        buffer += line;
        if(buffer.size() > BUFFER_THRESHOLD)
        {
            pages += processBuffer(buffer);
            chars += buffer.size();
            buffer.clear();
        }
        if((pages / 1000) > progressCount)
        {
            ++progressCount;
            std::cout << pages << " pages, ";
            std::cout << (chars / 1024 / 1024) << " mb" << std::endl;
        }
    }
    if(!buffer.empty())
    {
        pages += processBuffer(buffer);
        chars += buffer.size();
        std::cout << pages << " pages, ";
        std::cout << (chars / 1024 / 1024) << " mb" << std::endl;
    }
}