<-- Home

一些C++读写文本文件的优化技巧

下面是一个将文本文件读取到字符串中的简单C++函数:

std::string file_reader(char const* fname) {
	std::ifstream f;
	f.open(fname);
	if (!f) {
		std::cout << "Can't open " << fname
			<< " for reading" << std::endl;
		return "";
	}
	std::stringstream s;
	std::copy(std::istreambuf_iterator<char>(f.rdbuf()),
		std::istreambuf_iterator<char>(),
		std::ostreambuf_iterator<char>(s));
	return s.str();
}

fname是文件名。如果打不开文件,file_reader()会打印一条错误信息到标准输出中并返回空字符串。否则std::copy()会将f的流缓冲区复制到std::stringstream s的流缓冲区中。

file_reader()做了如下几件事:

  1. 打开文件
  2. 进行错误处理(以报告打开文件出错的形式)
  3. 读取已打开且有效的流到字符串中

如果file_reader()是一个库函数,这几种功能混合在一起会使得用户难以调用该函数。如果用户实现了自己的异常处理,或是想把错误信息输出到std::cerr中,就无法使用file_reader()。该函数还会申请一块内存然后返回它,返回值在调用链中传递的时候会被复制多次。我们可以分离打开文件处理与流读取处理。然后采用下面的方法优化。

使用std::swap()交换存储空间

void istream_reader(std::istream& f, std::string& result) {
	std::stringstream s;
	std::copy(std::istreambuf_iterator<char>(f.rdbuf()),
		std::istreambuf_iterator<char>(),
		std::ostreambuf_iterator<char>(s));
	std::swap(result, s.str());
}

最后一行使用std::swapresult的动态存储空间与s.str()的动态存储空间进行了交换。本来可以不进交换,将s.str()赋值给result即可,但是除非编译器和标准库实现都支持移动语义,否则这样做会导致内存分配和复制。

std::swap()对许多标准库类的特化实现都是调用它们的swap()成员函数。该成员函数会交换指针,这远比内存分配和复制操作的开销小。fstd::ifstream变成了std::istream。这个函数可以与 std::stringstream等其他类型的流一起工作。

缩短调用链

使用std::istream<<运算符来缩短调用链。<<运算符接收流缓冲区作为参数,而且可能会绕过istream的API直接调用流缓冲区。

void istream_reader(std::istream& f, std::string& result) {
	std::stringstream s;
	s << f.rdbuf();
	std::swap(result, s.str());
}

使用pubsetbuf()增大缓冲区

我们还可以通过增大输入缓冲区的大小来改善性能。

std::ifstream in8k;
in8k.open(filename);
char buf[8192];
in8k.rdbuf()->pubsetbuf(buf, sizeof(buf));

使用sgetn()读取数据

std::streambuf的成员函数sgetn(),它能够获取任意数量的数据到缓冲区参数中。对于一个普通大小的文件,读取整个文件只需一次函数调用。可以说是非常快了。

std::streamoff stream_size(std::istream& f) {
	std::istream::pos_type current_pos = f.tellg();
	if (-1 == current_pos)
		return -1;
	f.seekg(0, std::istream::end);
	std::istream::pos_type end_pos = f.tellg();
	f.seekg(current_pos);
	return end_pos - current_pos;
}

bool istream_reader(std::istream& f, std::string& result) {
	std::streamoff len = stream_size(f);
	if (len == -1)
		return false;
	result.resize(static_cast<std::string::size_type>(len));
	f.rdbuf()->sgetn(&result[0], len);
	return true;
}

sgetn()会直接将数据复制到result中,这要求result足够大才能存储下所有数据。因此必须在调用 sgetn()前确定流的大小。

使用接近底层的read()

bool istream_reader(std::istream& f, std::string& result) {
	std::streamoff len = stream_size(f);
	if (len == -1)
		return false;
	result.resize(static_cast<std::string::size_type>(len));
	f.read(&result[0], result.length());
	return true;
}



对于写文件来说,std::endl会刷新输出。如果没有std::endl,那么写文件应当会快很多,因为 std::ofstream 只是将几个大数据块传递给了操作系统。

使用\n代替std::endl

void ostream_writer(std::ostream& f, std::string const& line) {
	f << line << "\n";
}

解除std::cin和std::cout的绑定

当从标准输入中读取数据时,std::cin是与std::cout紧密联系在一起的。要求从std::cin中输入会首先刷新std::cout,这样交互控制台程序就会显示出它们的提示。 调用istream::tie()可以得到一个指向捆绑流的指针,前提是该捆绑流存在。调用istream::tie(nullptr)会打破已经存在的捆绑关系。

std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

C++流在概念上是与C的FILE*对象的stdinstdout连接在一起的,如果程序同时使用了C和C++的I/O函数,那么交叉行为产生的结果将变得不可预测。

结论

  1. 网上的“快速”文件I/O代码不一定快。
  2. 增大rdbuf的大小可以让读取文件的性能提高。
  3. 较快的读取文件的方法是预先为字符串分配与文件大小相同的缓冲区,然后调用std::streambuf::sgetn()函数填充字符串缓冲区。
  4. std::endl会刷新输出。如果你并不打算在控制台上输出,那么它的开销是昂贵的。
  5. std::cout是与std::cin捆绑在一起的。打破这种连接能够改善性能。但是和C的I/O函数混用会出现无法预料的结果。