3

我正在尝试从文件中读取 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;
}
4

3 回答 3

4

至少如果我没看错的话,我想我会采取一种更像 C 的方法。不是读取一行,然后将其分解为标记,并剥离您不想要的字符,而是一次读取一个字符,并根据我读取的字符,决定是否将其添加到当前标记,结束标记并将其添加到当前行,或结束行并将其添加到行向量:

#include <vector>
#include <string>
#include <stdio.h>
#include <time.h>

std::vector<std::vector<std::string> > read_tokens(char const *filename) {
    std::vector<std::vector<std::string> > lines;
    FILE *infile= fopen(filename, "r");

    int ch;

    std::vector<std::string> line;
    std::string token;

    while (EOF != (ch = getc(infile))) {
        switch(ch) {
            case '\n':
                lines.push_back(line);
                line.clear();
                token.clear();
                break;
            case '"':
                break;
            case '*':
                line.push_back(token);
                token.clear();
                break;
            default:
                token.push_back(ch);
        }
    }
    return lines;
}

int main() {
    clock_t start = clock();
    std::vector<std::vector<std::string> > lines = read_tokens("sample_tokens.txt");
    clock_t finish = clock();
    printf("%f seconds\n", double(finish-start)/CLOCKS_PER_SEC);
    return 0;
}

对您在评论中提供的样本的 20 万多份副本进行快速测试,它正在读取并(显然)使用 gcc 或 VC++ 约 4.5 秒在约 3.5 秒内标记数据。看到任何东西都变得更快(至少没有更快的硬件),我会有点惊讶。

顺便说一句,这是像您最初那样处理内存,这(至少在我看来)是非常有力的证据,表明在向量中管理内存可能不是主要瓶颈。

于 2012-09-10T03:16:50.297 回答
2

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

这是我为实现接近该要求而要做的事情。

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

因此,您的数据文件大约为 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 中,您可以使用CreateFileAPI 并对其进行调整以实现真正的快速读取。因此,我之前关于可能对齐数据块(与磁盘扇区大小)的评论。不确定 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 容器的内存池,我预计这些分配器的开销以及容器本身仍然会使它比我给您的要慢。很抱歉,答案很长。我想我只是喜欢这些东西。玩得开心,希望这会有所帮助。

于 2012-09-10T05:25:00.820 回答
2

而不是,每次调用都会boost::tokenizer<> tok(getcontent);构造一个新的. 使用成员函数:boost::tokenizergetlineassign

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

看看是否有帮助。此外,如果可能,请尝试预先分配向量内存。

于 2012-09-10T02:58:54.097 回答