C ++ - 为什么令牌化器从文件中读取行是如此之慢?

时间:2012-09-10 02:26:27

标签: c++ parsing tokenize

我正在尝试从文件中读取200,000条记录,然后使用tokenizer来解析字符串并删除每个部分周围的引号。但与正常读取字符串相比,运行时间非常长。只需要25秒读取这些记录(每条记录0.0001秒????)。我的编程是否有任何问题,或者如果没有更快的方法吗?

int main()
{
    int counter = 0;
    std::string getcontent;
    std::vector<std::string> line;
    std::vector< std::vector<std::string> > lines;

    boost::escaped_list_separator<char> sep( '\\', '*', '"' ) ;
    boost::tokenizer<> tok(getcontent);

    std::ifstream openfile ("test.txt");

    if(openfile.is_open())
    {
        while(!openfile.eof())
        {
            getline(openfile,getcontent);

            // THIS LINE TAKES A LOT OF TIME
            boost::tokenizer<> tok(getcontent); 

            for (boost::tokenizer<>::iterator beg=tok.begin(); beg!=tok.end(); ++beg){
                line.push_back(*beg);
            }

            lines.push_back(line);
            line.clear();
            counter++;
        }
        openfile.close();
    }
    else std::cout << "No such file" << std::endl;

    return 0;
}

2 个答案:

答案 0 :(得分:2)

而不是boost::tokenizer<> tok(getcontent);,它构建了对boost::tokenizer的每次调用的新getline。使用assign成员函数:

boost::escaped_list_separator<char> sep( '\\', '*', '"' ) ;
boost::tokenizer<boost::escaped_list_separator<char>> tok(getcontent, sep);

// Other code
while(getline(openfile,getcontent))
{
    tok.assign(getcontent.begin(), getcontent.end()); // Use assign here
    line.assign(tok.begin(), tok.end()); // Instead of for-loop
    lines.push_back(line);
    counter++;
}

看看是否有帮助。 另外,如果可能,请尝试事先分配矢量存储器。

答案 1 :(得分:2)

好的,从评论看来,你似乎想要一个尽可能快的解决方案。

以下是我要做的事情,以达到接近该要求的目的。

虽然你可能会得到一个内存池分配器来分配你的字符串,但STL并不是我的强项,所以我打算手工完成。要注意这不一定是C ++的方法。所以C ++ - 负责人可能会有点畏缩。有时,当你想要一些专门的东西时,你只需要这样做。

所以,你的数据文件大约是10 GB ......在一个块中分配它是一个坏主意。很可能你的操作系统会拒绝。但将它分解成一大堆相当大的块可以。也许这里有一个神奇的数字,但让我们说约64MB左右。传呼专家可以在这里发表评论吗?我记得曾经读过一次使用少于一个精确的页面大小倍数(虽然我不记得为什么),这是好的,所以让我们扯掉几个KB:

const size_t blockSize = 64 * 1048576 - 4096;

现在,跟踪记忆的结构怎么样?也可以把它作为一个列表,这样你就可以将它们全部放在一起。

struct Block {
    SBlock *next;
    char *data;    // Some APIs use data[1] so you can use the first element, but
                   // that's a hack that might not work on all compilers.
} SBlock;

是的,所以你需要分配一个块 - 你将分配一大块内存并使用第一个小块来存储一些信息。请注意,如果需要对齐内存,可以更改data指针:

SBlock * NewBlock( size_t blockSize, SBlock *prev = NULL )
{
    SBlock * b = (SBlock*)new char [sizeof(SBlock) + blockSize];
    if( prev != NULL ) prev->next = b;
    b->next = NULL;
    b->data = (char*)(blocks + 1);      // First char following struct
    b->length = blockSize;
    return b;
}

现在你要读......

FILE *infile = fopen( "mydata.csv", "rb" );  // Told you C++ers would hate me

SBlock *blocks = NULL;
SBlock *block = NULL;
size_t spilloverBytes = 0;

while( !feof(infile) ) {
    // Allocate new block.  If there was spillover, a new block will already
    // be waiting so don't do anything.
    if( spilloverBytes == 0 ) block = NewBlock( blockSize, block );

    // Set list head.
    if( blocks == NULL ) blocks = block;

    // Read a block of data
    size_t nBytesReq = block->length - spilloverBytes;
    char* front = block->data + spilloverBytes;
    size_t nBytes = fread( (void*)front, 1, nBytesReq, infile );
    if( nBytes == 0 ) {
        block->length = spilloverBytes;
        break;
    }

    // Search backwards for a newline and treat all characters after that newline
    // as spillover -- they will be copied into the next block.
    char *back = front + nBytes - 1;
    while( back > front && *back != '\n' ) back--;
    back++;

    spilloverBytes = block->length - (back - front);
    block->length = back - block->data;

    // Transfer that data to a new block and resize current block.
    if( spilloverBytes > 0 ) {
        block = NewBlock( blockSize, block );
        memcpy( block->data, back, spilloverBytes );
    }
}

fclose(infile);

好的,就像那样。你得到了这个量词。请注意,此时,您可能比多次调用std::getline要快得多地阅读文件。如果您可以禁用任何缓存,您可以更快地获得更快。在Windows中,您可以使用CreateFile API并对其进行调整以实现快速读取。因此我之前关于可能对齐数据块(与磁盘扇区大小)的评论。不确定Linux或其他操作系统。

因此,这是一种将整个文件粘贴到内存中的复杂方法,但它足够简单,易于访问且适度灵活。希望我没有犯太多错误。现在,您只想查看块列表并开始索引它们。

我不打算在这里详细介绍,但总体思路是这样的。您可以通过在适当的位置闪烁NULL值来跟踪就地标记,并跟踪每个标记的开始位置。

SBlock *block = blocks;

while( block ) {
    char *c = block->data;
    char *back = c + block->length;
    char *token = NULL;

    // Find first token
    while( c != back ) {
        if( c != '"' && c != '*' ** c != '\n' ) break;
        c++;
    }
    token = c;

    // Tokenise entire block
    while( c != back ) {
        switch( *c ) {
            case '"':
                // For speed, we assume all closing quotes have opening quotes.  If
                // we have closing quote without opening quote, this won't be correct
                if( token != c) {
                    *c = 0;
                    token++;
                }
                break;

            case '*':
                // Record separator
                *c = 0;
                tokens.push_back(token);  // You can do better than this...
                token = c + 1;
                break;

            case '\n':
                // Record and line separator
                *c = 0;
                tokens.push_back(token);  // You can do better than this...
                lines.push_back(tokens);  // ... and WAY better than this...
                tokens.clear();           // Arrrgh!
                token = c + 1;
                break;
        }

        c++;
    }

    // Next block.
    block = block->next;
}

最后,你会看到上面那些类似矢量的调用。现在,再次,如果你可以内存池,你的向量,这是伟大和容易的。但是再一次,我从来没有这样做,因为我觉得直接使用内存更加直观。您可以执行类似于我对文件块所做的操作,但为数组(或列表)创建内存。您将所有令牌(只有8个字节的指针)添加到此内存区域,并根据需要添加新的内存块。

您甚至可以创建一个小标题来跟踪其中一个令牌数组中有多少项。关键是永远不会计算一些你可以在以后计算的东西,无需额外费用(即数组大小 - 你只需要在添加最后一个元素后计算)。

你再次用线条做同样的事情。你需要的只是一个指向标记块中相关部分的指针(如果你想要数组索引,如果一行占用一个新的块,你必须做溢出事件。)

你最终得到的是一系列指向令牌数组的行,它直接指向你从文件中掏出的内存。虽然有一点内存浪费但它可能并不过分。这是你为快速编写代码所付出的代价。

我确信它可以在一些简单的课程中完美地包裹起来,但我已经把它给了你原始的。即使你把一堆STL容器的内存汇集起来,我预计这些分配器和容器本身的开销仍会比我给你的慢。很抱歉这个很长的答案。我想我只是喜欢这些东西。玩得开心,希望这会有所帮助。