【问题标题】:Fastest way to find the number of lines in a text (C++)查找文本行数的最快方法(C++)
【发布时间】:2009-05-09 11:28:46
【问题描述】:

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

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

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

【问题讨论】:

    标签: c++ line-count


    【解决方案1】:

    找到行数的唯一方法是读取整个文件并计算行尾字符的数量。 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;
    }
    

    【讨论】:

    • 我想补充一点,1MB 的大小应该是 1,048,576 字节而不是 1,000,000 字节。更准确地说,它应该是存储文件的设备的“块大小”的倍数。根据我的经验,硬盘驱动器为 512 字节,CD/DVD 驱动器为 2048 字节。不知道这是否在某些规范中。
    • 另外,你不应该一次读一个块。每次读取操作都有固定的开销,因此一次读取的数据越多,处理速度就越快。但是,我还发现如果一次读取 400 个块或一次读取 800 个块,整体速度(对于 HDD)几乎没有差异。所以我想 1,048,576 字节就绰绰有余了。
    • 上次我玩这个游戏时,我测试了很多缓冲区大小,以找到我们获得最大收益的最佳点(RAM 和速度都非常重要)。这是输出到闪存的,所以结果不适用于这个问题,但我发现你得到了最大可能速度的 90% 左右和最大有益内存的 25% 左右。所以这完全取决于两者如何权衡。当然,1MB 在 PC 上不算什么。
    • 实际上,发布的代码有点缺陷 - 大缓冲区初始化为零(因为它是一个向量)。您可以改用 new char[SZ] 来删除启动命中。我的借口是我有一些代码(缓冲区小得多),我做了一个快速复制和粘贴。
    • 您可以将CountLines替换为std::count(buf.begin(), buf.end(), '\n')
    【解决方案2】:

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

    然后使用magic algorithms 'SIMD Within A Register (SWAR) Operations' 之一以系统的本机字大小(即uint32_t 或uint64_t)扫描块,以测试字内的字节.一个例子是here;带有0x0a0a0a0a0a0a0a0aLL 的循环会扫描换行符。 (该代码达到每个输入字节大约 5 个周期,匹配文件每一行上的正则表达式)

    如果文件只有几十或一百多兆字节,并且它一直在增长(即不断向其写入内容),那么 linux 很可能已将其缓存在内存中,因此不会磁盘 IO 有限,但内存带宽有限。

    如果文件只是被附加到,你还可以记住行数 和之前的长度,然后从那里开始。


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

    【讨论】:

    • 没有理由不能在 C++ 中使用 mmap 或读取块。
    • 在 stl 容器和字符串和原始 mmapped 内存之间进行转换,您必须跳过很多环节,这通常涉及复制和间接,而不仅仅是调用函数并使用 C 直接使用内存C++ 的子集。
    • 当然没有 C++ 的“C 子集”。仅仅因为您不使用 std 容器或字符串并不能使其在某种程度上“不是 C++”。
    • 补充一点 Neil 所说的,std 算法在原始内存上工作得非常好,使用指针作为迭代器。您可以简单地编写一个执行指定 SIMD 技巧的函子,并使用 std::for_each 对文件数据运行它。 STL 不仅仅是容器
    • @Neil 你很清楚有一个 C++ 的子集接近惯用的 C 我之前已经发布了这个确切的响应,并因为它是 C 而不是 C++ 而受到批评;你赢不了。
    【解决方案3】:

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

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

    【讨论】:

    • 是的,它就像一个日志文件(但不是日志文件:来自多个开发人员的多个日志文件的统计信息的聚合),该程序由另一个人开发并由他维护。但是您需要一个可行的本地解决方案。但我只是想知道一个全局最优解决方案,它可以非常快速地计算而无需对文件有先验知识。这只是为了好奇:)。
    【解决方案4】:

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

    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 FEED 或 U+000B LINE TABULATION(也称为垂直制表符)是否应该考虑换行?

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

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

    已接受答案中的代码存在大多数这些陷阱。在快速完成之前先做好准备。

    【讨论】:

    • +1,优秀的分数。 #3 吓到我了——我第一次听说!
    • @j_random_hacker 是的,我也是! :O
    • 关于 pt 2 值得注意的是:如果一行没有以换行符结尾,则根据 POSIX 标准的定义,它不是一行,它是一个不完整的行,它是一个非常很好的理由。一行是零个或多个以 0x0a 结尾的字节。文本文件应以换行符结尾。有些人为了能够编写例如不以换行符结尾的代码文件而竭尽全力。这些可能会破坏或破坏各种工具/链。所以通过那个“你好,世界!”是零行而不是一;)
    • @user3342816:Posix 只是处理文本文件的系统的一个子集。在互联网世界中,人们共享来自各种系统的文件(并且网络协议通常使用“\x0D\x0A”作为行分隔符),明智的做法是考虑有许多其他标准。跨度>
    • 当然。但是认为您可能会错过通过评论来解释我的意图。如果不知道文件的“格式/来源”,就无法计算行数。 OP 在 Linux 上,文本文件通常由以 0x0a 结尾的行组成。如果有人在 Linux 上使用 Windows 文本文件,通常会先转换它们。至于网络协议,确保它们主要使用 0x0d0a,但这不是用于有效负载的。它们(大多数?)不处理行,而是处理字节。我绝不是说这是一个糟糕的答案,但我认为至少在 cmets 中值得一提。
    【解决方案5】:

    请记住,所有 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.
    }
    

    【讨论】:

    • 我针对我发布的两种可能的实现运行了您的代码。你的运行速度与 getline() 版本相同(即显式缓冲速度的一半),但不幸的是,我在我的百万行文本文件上发布的实现也打印出错误的行数 - 1000001 与 1000000,其中windows 格式会被 CR/LF 终止。
    • PS 似乎认为 test.last 包含零字节。
    • 这是因为文件是以文本模式打开的。这会强制进行需要处理的 EOL 翻译。如果文件以二进制模式打开,则没有 EOL 转换,它会更快。您需要 EOL 翻译的天气或将取决于您在做什么。
    • @Neil:windows 格式由 CR/LF 终止:此序列应由流自动转换为 '\n',因为它是 EOL 标记。如果您将文件从一个操作系统移动到另一个操作系统,那么所有的赌注都没有了,您应该在使用前转换您的文件。见 dos2unix
    【解决方案6】:

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

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

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

    【讨论】:

    • 问题是关于使运行时更快,而不是算法如何扩展。每个 n 需要几个周期的 O(n) 算法比每个 n 需要一年的算法要快。
    【解决方案7】:

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

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

    1/ 如果您使用的是简单循环,一次读取一个字符以检查换行符,请不要这样做。尽管 I/O 可能会被缓冲,但函数调用本身在时间上是昂贵的。

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

    2/ 如果您说一般行长约为 40-50 个字符并且您不需要 精确 行数,只需获取文件大小并除以 45(或无论您认为使用什么平均值)。

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

    例如,当它达到 5M 时,将它(例如,x.log)移动到一个有日期的文件名(例如,x_20090101_1022.log)并计算出该点有多少行(将其存储在 @987654324 @,然后开始一个新的x.log 日志文件。日志文件的特性意味着创建的这个过时的部分永远不会改变,因此您永远不必重新计算行数。

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

    【讨论】:

      【解决方案8】:

      需要时间的事情是将 40+ MB 加载到内存中。最快的方法是要么对其进行内存映射,要么将其加载到一个大缓冲区中。一旦你把它放在内存中,不管它是如何实现的,一个遍历数据寻找\n字符的循环几乎是瞬时的。

      所以说真的,最重要的技巧是尽可能快地将文件加载到内存中。最快的方法是将其作为单个操作来完成。

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

      或者也许您可以维护一个单独的索引文件,显示已知 '\n' 字符的位置,以便可以跳过文件的这些部分。

      从硬盘读取大量数据很慢。没有办法。

      【讨论】:

      • 超过某个点,使用更大的缓冲区实际上会导致更慢的性能,因为更少的 RAM 将适合 L2 缓存。对于非常大的缓冲区,操作系统可能需要分页到磁盘以便为您提供请求的 RAM 量
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-24
      • 2015-09-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多