如何优化C++逐行读取文件到向量中

How can I optimize C++ to read a file line by line into a vector?

本文关键字:文件 读取 向量 逐行 C++ 何优化 优化      更新时间:2023-10-16

令我惊讶的是,我注意到以下C++只是读取一个大文件行,将行存储到一个向量中并输出向量大小,运行速度比Python慢得多。如何优化?谢谢

#include <fstream>
#include <iostream>
#include <vector>
#include <string>
   int main () {
    std::ifstream infile("longfile.txt");
    std::string line;
    std::vector<std::string> lines;
    while (std::getline(infile, line)) {
        lines.push_back(line);
    }
    std::cout << lines.size() << std::endl;
    return 0;
}

跑:

$ time ./a.out
1390000
real    0m6.388s
user    0m6.130s
sys 0m0.243s

蟒:

with open('longfile.txt') as f:
    lines = f.readlines()
print len(lines)

跑:

$ time python test.py
1390000
real    0m1.003s
user    0m0.158s
sys     0m0.146s

如果你能提前知道你必须读取的行数,你可以在你的std::vector对象中保留行数。假设您有 10 行要阅读:

   int main () {
    std::ifstream infile("longfile.txt");
    std::string line;
    std::vector<std::string> lines;
    lines.reserve(10);
    while (std::getline(infile, line)) {
        lines.push_back(line);
    }
    std::cout << lines.size() << std::endl;
    return 0;
}

像这样,您的内存将在您开始工作之前分配。小心reserve不是resize.我的意思是lines.reserve(10);会提前 10 std::string分配,但lines.empty()仍然是真的。

如果你不能提前知道行数std::list(双链表),或者std::forward_list(简单链接表)会有所帮助。每个新元素都将添加到列表中,直到您完成读取文件。在列表中,无需重新分配内存,每次达到最大容量时都必须对std::vector执行的操作。就时间而言,重新分配和复制内存中的元素非常昂贵。使用列表,您至少可以减少解析文件所花费的时间。

使用将列表复制到std::vector解析文件后,最好将列表复制到因为您已经知道需要的内存大小,连续的内存分配速度更快。

无论您选择什么,我都强烈建议您更改:

while (std::getline(infile, line)) {
    lines.push_back(line);
}

由:

    while (std::getline(infile, line)) {
        lines.push_back(std::move(line));
    }

默认情况下,对于 STL 中的大多数容器,复制构造函数或赋值运算符的调用将完全复制数据。 std::move防止此类复制。

您可以使用以下示例轻松检查它:

std::string a("Hello");
std::string b(a);
std::cout<<a.size()<<" "<<b.size()<<std::endl;
std::cout<<"the address of a is : "<<a.c_str()<<" "<<b.c_str()<<std::endl;
std::string d(std::move(a));
std::cout<<a.size()<<" "<<d.size()<<std::endl;
std::string e;
std::string f;
e = b;
std::cout<<e.size()<<" "<<b.size()<<std::endl;
std::cout<<"the address of a is : "<<e.c_str()<<" "<<b.c_str()<<std::endl;

f = std::move(b);
std::cout<<f.size()<<" "<<b.size()<<std::endl;

几乎所有优化问题的答案都是"首先,配置文件"。分析您的C++应用程序并确定花费时间的位置。

不过,我可以对这里的慢速进行一些有根据的猜测,并指出这将如何显示在探查器中。

慢速获取线()

getline()可能以缓慢的方式实现。例如,它可能需要一次要求运行时输入一个字符,因为一旦出现换行符,它就需要停止读取。也就是说,它不能要求更大的块中的字节,因为当块中间出现换行符时,它无法保证"放回"块的其余部分。

运行时几乎肯定会缓冲基础文件读取,因此这不会像每个字符一个系统调用那么糟糕,但为文件中的每个字符有效调用getc的开销仍然很大。

这将在探查器中显示为花费在getline()上的大量时间 - 特别是在getline调用的某些类似getc()的方法中。

python 实现根本没有这个问题,因为进行了一次readlines()调用,并且实现知道整个文件将被读取并且可以随意缓冲。

冗余复制

另一个可能的候选者是冗余复制。

第一个运行时进行read()调用,并将文件块复制到内部缓冲区中。然后,getline()实现可能会有一个 char[] 的内部缓冲区,在将字符串传递给 string 构造函数之前,它会构建字符串,这可能会制作另一个副本(除非运行时使用内部技巧直接交出缓冲区)。

然后,正如Johnny_S指出的那样,当您将这些字符串推送到向量中时,可能会有更多的副本。

这将显示在向量中,因为花费的时间在上述各种副本中分布,例如,在string()构造函数中。

python 实现还可以避免大多数这些冗余副本,因为它对问题具有更高层次的视图,而不是C++实现中的分层方法,因此它可能只制作 1 或 2 个副本。

解决 方案

这里提到的解决方案解决了上述两个问题。要重新实现 Python readlines 调用,您应该降低一点级别。以char[]块的形式读取文件,并直接在缓冲区中查找换行符。从技术上讲,您根本不需要创建string对象,因为您只输出找到的行数,但是如果您确实想要创建这些对象,请确保只将char[]数据复制到每个字符串中一次。

可以使用 string (const char* s, size_t n) 构造函数直接指向字符缓冲区来执行此操作。最后,确保在复制到载体时不要再复制,正如Johnny_S所建议的那样。