用C++读取文件的花式方法:奇怪的性能问题

11

在C++中读取文件的通常方法如下:

std::ifstream file("file.txt", std::ios::binary | std::ios::ate);
std::vector<char> data(file.tellg());
file.seekg(0, std::ios::beg);
file.read(data.data(), data.size());

读取一个1.6 MB的文件几乎是瞬间完成的。

但是最近,我发现了std::istream_iterator,想尝试使用它来编写一种优美的一行代码方式来读取文件的内容。像这样:

std::vector<char> data(std::istream_iterator<char>(std::ifstream("file.txt", std::ios::binary)), std::istream_iterator<char>());

代码很好,但是非常慢。读取同样大小的1.6 MB文件大约需要2/3秒钟。我知道这可能不是最好的读取文件的方式,但为什么会这么慢呢?
传统的文件读取方式是这样的(我只谈论read函数):
- istream包含一个filebuf,它包含来自文件的数据块。 - read函数从filebuf调用sgetn,将字符一个一个地复制(没有memcpy),从内部缓冲区复制到"data"的缓冲区。 - 当filebuf中的数据完全读取时,filebuf会从文件中读取下一个块。
当使用istream_iterator读取文件时,过程如下:
- 向量调用*iterator以获取下一个字符(这只是读取一个变量),将其添加到末尾并增加自身大小。 - 如果向量分配的空间已满(这不太常见),则执行重定位。 - 然后调用++iterator从流中读取下一个字符(使用char参数的operator >>,这肯定只是调用filebuf的sbumpc函数)。 - 最后,它比较迭代器和结束迭代器,通过比较两个指针来完成。

我必须承认第二种方法并不是很高效,但它至少比第一种方式慢200倍,这怎么可能呢?

我以为性能杀手是重定位或插入,但我尝试创建整个向量并调用std::copy,速度也同样慢。

// also very slow:
std::vector<char> data2(1730608);
std::copy(std::istream_iterator<char>(std::ifstream("file.txt", std::ios::binary)), std::istream_iterator<char>(), data2.begin());

我认为性能杀手是重定位或插入,这就是为什么你需要依赖于分析。 - Mark Ransom
3个回答


3
只有分析才能告诉你确切原因。我的猜测是,你看到的只是第二种方法所关联的所有额外函数调用的开销。你不是执行一次调用获取所有数据,而是执行了160万次调用*...或者类似的内容。
*其中许多是虚拟的,这意味着每个调用需要两个 CPU 周期。(感谢 Zan)

1
是的,那些调用是间接虚拟函数。那些很糟糕。 - Zan Lynx
1
性能分析告诉我许多函数占总体的 3 到 5%,没有任何函数排名第一;我将您标记为已接受的答案。 - Tomaka17

1

迭代器方法一次读取一个字符,而file.read则一次性读取。

如果操作系统/文件处理程序知道您要读取大量数据,则可以进行许多优化 - 可能在磁盘主轴的单个旋转中读取整个文件,而不是将数据从OS缓冲区复制到应用程序缓冲区。

当您进行逐字节传输时,操作系统不知道您真正想要做什么,因此无法执行此类优化。


1
fstream 中的 filebuf 对象按块读取文件。无论哪种情况下读取文件的方式都是相同的,只是从 filebuf 复制到 vector<char> 的过程比较慢。 - Tomaka17

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接