【问题标题】:How can I optimize C++ to read a file line by line into a vector?如何优化 C++ 以将文件逐行读取到向量中?
【发布时间】:2016-06-08 13:33:15
【问题描述】:

令我惊讶的是,我注意到以下 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

Python:

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

【问题讨论】:

  • 不,我事先不知道。
  • Python readlines 函数所做的基本上就是您的 C++ 程序所做的。如果您不知道开始的行数,并且如果它们的大小不完全相同,那么真的没有办法优化它。
  • 一些问题:(1)你是如何编译C++程序的,你是否使用了像-O2这样的优化标志?如果不是,那么比较是没有意义的。 (2)你的时间值,你能重复并确认它们吗? Python 可能会更快,因为到第二次运行时,数据已缓存在内存中。我们需要更多信息。
  • 以交替顺序重复运行这两个测试,以排除由于磁盘和/或操作系统级别的缓存而产生的任何奇怪影响。
  • 试试std::deque 它不需要像vector需要的那样将内部存储组织在一个连续的内存区域中。

标签: python c++ optimization stream


【解决方案1】:

如果您可以提前知道必须阅读的行数,则可以在 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::stringlines.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;

【讨论】:

  • std::vector 通常仍然比std::list 和变体更快,即使您事先不知道大小。插入 N 个元素的 O() 性能是相同的:在这两种情况下都是 O(N),但是 vector 定期分配大块内存的方法比列表每个节点分配快得多。
【解决方案2】:

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

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

慢 getline()

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 的建议,当您复制到向量中时,请确保不要再复制一份。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-21
    • 2012-06-13
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    相关资源
    最近更新 更多