查找文本行数的最快方法(C ++)

时间:2009-05-09 11:28:47

标签: c++ line-count

在对该文件执行某些操作之前,我需要读取文件中的行数。当我尝试读取文件并在每次迭代时递增line_count变量,直到我达到eof。在我的情况下,这并不是那么快。我同时使用了ifstream和fgets。他们都很慢。有没有一种hacky方法可以做到这一点,例如BSD,Linux内核或berkeley db也可以使用它(可以使用按位运算)。

正如我之前所说,该文件中有数百万行,并且它会不断变大,每行大约有40或50个字符。我正在使用Linux。

注意: 我相信会有人会说使用数据库白痴。但在我的情况下,我不能使用数据库。

8 个答案:

答案 0 :(得分:17)

查找行数的唯一方法是读取整个文件并计算行尾字符的数量。 tom执行此操作的最快方法可能是将整个文件读入一个具有一次读取操作的大缓冲区,然后通过缓冲区计算'\ n'字符。

由于您当前的文件大小约为60Mb,因此这不是一个有吸引力的选项。你可以通过不读取整个文件来获得一些速度,但是可以读取它的大小。比如大小为1Mb。你还说数据库是不可能的,但它确实看起来是最好的长期解决方案。

编辑:我刚刚对此进行了一个小的基准测试,并且使用缓冲方法(缓冲区大小为1024K)似乎比使用getline一次读取一行快两倍( )。这是代码 - 我的测试是使用g ++使用-O2优化级别完成的:

#include <iostream>
#include <fstream>
#include <vector>
#include <ctime>
using namespace std;

unsigned int FileRead( istream & is, vector <char> & buff ) {
    is.read( &buff[0], buff.size() );
    return is.gcount();
}

unsigned int CountLines( const vector <char> & buff, int sz ) {
    int newlines = 0;
    const char * p = &buff[0];
    for ( int i = 0; i < sz; i++ ) {
        if ( p[i] == '\n' ) {
            newlines++;
        }
    }
    return newlines;
}

int main( int argc, char * argv[] ) {
    time_t now = time(0);
    if ( argc == 1  ) {
        cout << "lines\n";
        ifstream ifs( "lines.dat" );
        int n = 0;
        string s;
        while( getline( ifs, s ) ) {
            n++;
        }
        cout << n << endl;
    }
    else {
        cout << "buffer\n";
        const int SZ = 1024 * 1024;
        std::vector <char> buff( SZ );
        ifstream ifs( "lines.dat" );
        int n = 0;
        while( int cc = FileRead( ifs, buff ) ) {
            n += CountLines( buff, cc );
        }
        cout << n << endl;
    }
    cout << time(0) - now << endl;
}

答案 1 :(得分:9)

不要使用C ++ stl字符串和getline(或C的fgets),只使用C样式的原始指针,并在页面大小的块中块读取或mmap文件。

然后使用magic algorithms'SIMD IN A Register(SWAR)操作'之一扫描系统本机字大小的块(即uint32_tuint64_t)测试单词中的字节。一个例子是here;其中带有0x0a0a0a0a0a0a0a0aLL的循环会扫描换行符。 (该代码每个输入字节大约需要5个周期,与文件每行的正则表达式相匹配)

如果文件只有几十或一百左右的兆字节,并且它不断增长(即某些内容一直在写入),那么linux很可能将它缓存在内存中,所以它不会磁盘IO有限,但内存带宽有限。

如果只附加文件,您还可以记住行数 和以前的长度,从那里开始。


有人指出,您可以将mmap与C ++ stl算法一起使用,并创建一个传递给std :: foreach的仿函数。我建议你不要这样做,不是因为你不能这样做,而是写这些额外的代码没有收获。或者你可以使用boost的mmapped迭代器,它可以为你处理它;但是对于这个问题,我链接到的代码是为了这个而写得慢得多,问题是关于速度而不是风格。

答案 2 :(得分:9)

你写道它会不断变大。 这听起来像是一个日志文件或类似的东西,其中添加了新行但现有行不会更改。如果是这种情况,您可以尝试增量方法

解析到文件末尾。 记住行数和EOF的偏移量。 当文件增加fseek到偏移量时,解析为EOF并更新行数和偏移量。

答案 3 :(得分:6)

计数线和计数线分隔符之间存在差异。如果获得精确的行数,需要注意的一些常见问题很重要:

  1. 文件编码是什么?逐字节解决方案适用于ASCII和UTF-8,但请注意,如果您使用UTF-16或某些多字节编码,并不能保证具有换行值的字节必须编码换行符。

  2. 许多文本文件在最后一行的末尾没有行分隔符。因此,如果您的文件显示为"Hello, World!",则最终可能会计为0而不是1.而不是仅计算行分隔符,您需要一个简单的状态机来跟踪。

  3. 一些非常模糊的文件使用Unicode U+2028 LINE SEPARATOR(甚至U+2029 PARAGRAPH SEPARATOR)作为行分隔符,而不是更常见的回车符和/或换行符。您可能还需要留意U+0085 NEXT LINE (NEL)

  4. 您必须考虑是否要将其他控制字符计为断路器。例如,是否应将U+000C FORM FEEDU+000B LINE TABULATION(a.k.a。垂直标签)视为新行?

  5. 旧版Mac OS(OS X之前)的文本文件使用回车符(U+000D)而不是换行符(U+000A)来分隔行。如果您正在将原始字节读入缓冲区(例如,您的流处于二进制模式)并扫描它们,那么您将在这些文件上计数为0。您不能同时计算回车和换行,因为PC文件通常以两者结束。同样,你需要一个简单的状态机。 (或者,您可以在文本模式而不是二进制模式下读取文件。文本接口会将行分隔符规范化为'\n',以获取符合平台上使用的约定的文件。如果您正在从其他平台读取文件,您将使用状态机返回二进制模式。)

  6. 如果文件中有超长行,getline()方法可能会引发异常,导致简单行计数器在少量文件上失败。 (如果您在非Mac平台上阅读旧的Mac文件,尤其如此,导致getline()将整个文件视为一个巨大的行。)通过将块读取到固定大小的缓冲区并使用状态机,你可以使它成为防弹。

  7. 接受的答案中的代码会受到大多数陷阱的影响。在你加速之前做好准备。

答案 4 :(得分:4)

请记住,所有fstream都是缓冲的。因此它们实际上实际上读取了块,因此您不必重新创建此功能。所以你需要做的就是扫描缓冲区。不要使用getline(),因为这会强制你调整字符串的大小。所以我只使用STL std :: count和流迭代器。

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>


struct TestEOL
{
    bool operator()(char c)
    {
        last    = c;
        return last == '\n';
    }
    char    last;
};

int main()
{
    std::fstream  file("Plop.txt");

    TestEOL       test;
    std::size_t   count   = std::count_if(std::istreambuf_iterator<char>(file),
                                          std::istreambuf_iterator<char>(),
                                          test);

    if (test.last != '\n')  // If the last character checked is not '\n'
    {                       // then the last line in the file has not been 
        ++count;            // counted. So increement the count so we count
    }                       // the last line even if it is not '\n' terminated.
}

答案 5 :(得分:3)

由于您的算法速度不慢,因为IO操作很慢,所以速度很慢。我想你正在使用一个简单的O(n)算法,它只是按顺序遍历文件。在这种情况下,没有更快的算法可以优化您的程序。

然而,我说没有更快的算法,但有一个更快的机制,称为“内存映射文件”,映射文件有一些缺点,它可能不适合你的情况,所以你必须阅读它并自己弄清楚。

内存映射文件不允许您实现比O(n)更好的算法,但可能将减少IO访问时间。

答案 6 :(得分:3)

您只能通过扫描整个文件来查找换行符来获得明确的答案。没有办法解决这个问题。

但是,您可能需要考虑几种可能性。

1 /如果你使用的是一个简单的循环,一次读取一个字符来检查换行,不要。即使可以缓冲I / O,函数调用本身也是昂贵的,时间紧迫。

更好的选择是通过单个I / O操作将大块文件(比如5M)读入内存,然后进行处理。您可能不需要过多担心特殊汇编指令,因为无论如何都会优化C运行时库 - 一个简单的strchr()应该这样做。

2 /如果你说一般行长度大约是40-50个字符而你不需要完全行数,那么只需抓取文件大小并除以45(或者无论你想要使用什么平均值。)

3 /如果这类似于日志文件,并且您没有保存在一个文件中(可能需要在系统的其他部分进行返工),请考虑定期拆分文件。

例如,当它达到5M时,将它(例如,x.log)移动到日期文件名(例如x_20090101_1022.log)并计算出该点有多少行(存储)它在x_20090101_1022.count中,然后启动一个新的x.log日志文件。日志文件的特征意味着创建的这个日期部分永远不会改变,所以你永远不必重新计算行数。

要处理日志“文件”,您只需cat x_*.log通过某个流程管道而不是cat x.log。要获取“文件”的行数,请在当前x.log上执行wc -l(相对较快),并将其添加到x_*.count文件中所有值的总和。

答案 7 :(得分:1)

花费时间的东西是将40多MB加载到内存中。最快的方法是对其进行内存映射,或者将其加载到一个大缓冲区中。一旦你在内存中有这种或那种方式,遍历数据寻找\n字符的循环几乎是瞬时的,无论它是如何实现的。

实际上,最重要的技巧是尽快将文件加载到内存中。最快的方法是将其作为单个操作。

否则,可能存在大量技巧来加速算法。如果仅添加行,从不修改或删除行,并且如果您重复读取文件,则可以缓存先前读取的行,并且下次必须读取文件时,只读取新添加的行。

或许你可以维护一个单独的索引文件来显示已知'\ n'字符的位置,这样就可以跳过文件的那些部分。

从硬盘驱动器读取大量数据的速度很慢。没有办法解决这个问题。