使用 fstream 读取完整文件并转换为字符串的最佳方法是什么?

10

许多其他帖子,例如" 将整个ASCII文件读入C++ std::string "等解释了一些选项,但没有深入描述各种方法的优缺点。我想知道为什么一种方法比另一种更可取?

所有这些方法都使用std::fstream将文件读入std::string中。我不确定每种方法的成本和收益。假设这是针对已知大小适中的文件而言,内存可以轻松容纳,显然,不管你如何做,将多TB的文件读入内存都是一个坏主意。

在几次谷歌搜索后,将整个文件读入std::string中最常见的方法涉及使用std::getline并在每行后附加换行符。这对我来说似乎是不必要的,但是否有性能或兼容性原因使其成为理想选择?

std::string Results;
std::ifstream ResultReader("file.txt");    
while(ResultReader)
{
    std::getline(ResultReader, Results);
    Results.push_back('\n');
}

我另一种方法是更改getline的定界符,使其不是文件中的任何内容。EOF字符似乎不太可能出现在文件中间,因此它似乎是一个可能的选择。这包括一个类型转换,因此至少有一个理由不要这样做,但是这将一次性读取文件,而无需进行字符串连接。假设定界符检查仍然会有一些成本。还有其他好的理由不这样做吗?

std::string Results;
std::ifstream ResultReader("file.txt");
std::getline(ResultReader, Results, (char)std::char_traits<char>::eof());

如果系统将std::char_traits::eof()定义为其他值而不是-1,则可能存在问题,这就是选择使用std::getlinestring::push_pack('\n')等其他方法的实际原因。

相较于像这个问题中读取整个ASCII文件到C++ std::string的其他方式,它们之间有什么区别?

std::ifstream ResultReader("file.txt");
std::string Results((std::istreambuf_iterator<char>(ResultReader)),
                     std::istreambuf_iterator<char>());

看起来这是最佳选择。它将几乎所有工作都转移到了标准库上,该库应针对给定平台进行了大量优化。我认为除流的有效性检查和文件结尾外,没有理由进行其他检查。这是否理想,或者有看不见的问题。

标准或某些实现的细节是否提供了推荐一种方法胜过另一种的原因?我是否错过了某种可能在各种情况下都非常理想的方法?

读取整个文件到 std::string 的最简单、最典型、表现最佳且符合标准的方法是什么?

编辑 - 2 这个问题促使我编写了一个小型基准套件。它们采用 MIT 许可证,并可在 github 上获得: https://github.com/Sqeaky/CppFileToStringExperiments

最快 - TellSeekRead 和 CTellSeekRead - 系统提供了易于获取的大小并一次性读取文件。

更快 - Getline 追加和 Eof - 检查字符似乎不会带来任何成本。

- RdbufMove 和 Rdbuf - std::move 在发布时似乎没有任何区别。

- Iterator、BackInsertIterator 和 AssignIterator - 迭代器和输入流有些问题。它们在内存中表现很好,但在这里不行。尽管如此,其中一些比其他的更快。

我已添加到目前为止建议的每种方法,包括链接中的方法。如果有人能在 Windows 上或使用其他编译器运行此程序,则会非常感激。我当前无法访问具有 NTFS 的计算机,而且已经注意到这个和编译器细节可能很重要。

至于测量简单性和典型性,我们如何客观地衡量这些?简单性似乎是可行的,可能使用类似 LOC 和圆形度复杂性之类的东西,但某件事是否典型纯粹是主观的。


4
可能是将整个ASCII文件读入C++ std :: string的重复问题。 - Chris Drew
1
链接的答案使用 seek/tell 查找文件长度。如果您知道它是一个常规文件,则更简单的方法是使用 stat。 - stark
1
stat符合标准,但标准是POSIX。 - user4581301
1
我怀疑的是斯塔克的评论是否合格。 - user4581301
1
我应该回复你们两个,我并不是想挑刺。即便如此,对于许多人来说,“stat”也是一个可行的答案。 - Sqeaky
显示剩余3条评论
3个回答

5
什么是读取整个文件存入std::string对象的最简单、最惯用、最高效和标准兼容的方法?
这些要求很难同时满足,其中一个可能会削弱另一个。简化的代码不会是最快的,也不一定最惯用。
经过一段时间的探索,我得出了以下结论:
1)最影响性能的是IO操作本身——采用较少的IO操作速度更快
2)内存分配也相当昂贵,但不如IO那么昂贵
3)二进制读取比文本读取更快
4)使用操作系统API可能比C++流更快
5)std::ios_base::sync_with_stdio并没有真正影响性能,这是一个流传已久的谬论。
如果需要高性能,则使用std::getline可能不是最佳选择,因为它会为N行生成N个IO操作和N个内存分配。
一种快速、标准和优雅的折中方案是获取文件大小,一次性分配所有内存,然后一次性读取文件。
std::ifstream fileReader(<your path here>,std::ios::binary|std::ios::ate);
if (fileReader){
  auto fileSize = fileReader.tellg();
  fileReader.seekg(std::ios::beg);
  std::string content(fileSize,0);
  fileReader.read(&content[0],fileSize);
}   

移动内容以避免不必要的复制。


我将此添加到我在问题中提供的基准测试套件中。我同意这种方法很好,也是目前最快的,但我不同意你的一些观点。我认为二进制并不比文本更快,在1000次迭代中,毫秒级别上没有任何区别。我认为对于整个问题的答案可能就像你的第一点那样简单。 - Sqeaky
std::string(size_t, char) constructor not only allocates and sets size, but also fills the allocated memory with the given char. I would use std::unique_ptr<char[]>(new char[fileSize]); or maybe make_unique - that way you will have exception safety and also avoid initializing the potentially large buffer with '\0' - Roman Kruglov
在块内定义content会在块结束时销毁它,因此任何使用它的代码都需要在该块中编写,对吗? - rwst

2

这个网站对几种不同的方法进行了很好的比较。我目前使用的是:

std::string read_sequence() {
    std::ifstream f("sequence.fasta");
    std::ostringstream ss;
    ss << f.rdbuf();
    return ss.str();
}

如果您的文本文件是按换行符分隔的,这将保持它们。如果您想要删除它,例如(在我大多数情况下是这样的),您可以添加一个调用类似于以下内容的东西。
auto s = ss.str();
s.erase(std::remove_if(s.begin(), s.end(), 
        [](char c) { return c == '\n'; }), s.end());

1
我会阅读您的网站,感谢lambda remove_if表达式,这是一个简单的方法来实现这样的任务。您的读取缓冲区到stringstream的方法似乎与Max的方法没有实质性的不同,std::move似乎并没有做任何好的编译器不已经做的事情。我添加了RdbufMove作为测试到基准套件中,这个问题让我写:https://github.com/Sqeaky/CppFileToStringExperiments - Sqeaky
使用mmap和子类字符串以正确的方式运行。Windows似乎有类似的功能。 - msw
@msw,我不知道你说的是什么意思,也没有Windows机器可以使用。你能否请解释一下? - Sqeaky
1
@Sqeaky 你说得对,那里的 std::move 是不必要的。谢谢你指出来 :-) - LLLL

1

你的问题存在两个困难。首先,标准没有规定任何特定的实现方式(是的,几乎所有人都从同样的实现方式开始;但他们随着时间的推移进行了修改,因此对于NTFS来说,最佳I/O代码与ext4的最佳I/O代码将不同),因此可能(虽然有点不太可能)某种方法在一个平台上最快,但在另一个平台上则不是最快的。其次,“最优”的定义有一点困难;我认为你的意思是“最快”,但这并不一定是这样。

有些方法在C++中是惯用的,完全没有问题,但不太可能给出出色的性能。如果您的目标是最终获得一个单独的std::string,使用std::getline(std::ostream&, std::string&)很可能比必要的慢。 std::getline()调用必须查找'\n',并且偶尔需要重新分配和复制目标std::string。即便如此,它非常简单易懂。从维护的角度来看,这可能是最优的,假设您不需要绝对最快的性能。如果您不需要一次性在一个巨大的std::string中获取整个文件,那么这也是一个好方法。您将非常节省内存。
一种更有效的方法是操作读取缓冲区:
std::string read_the_whole_file(std::ostream& ostr)
{
    std::ostringstream sstr;
    sstr << ostr.rdbuf();
    return sstr.str();
}

个人而言,我可能会使用std::fopen()std::fread()(以及std::unique_ptr<FILE>),因为至少在Windows上,当std::fopen()失败时,你会得到更好的错误消息,而不是构造文件流对象失败。我认为更好的错误消息是决定哪种方法最优的重要因素。

1
我将这个程序和我写的三个方法都写入了一个微基准测试中:https://github.com/Sqeaky/CppFileToStringExperiments。你有NTFS格式的计算机吗?我没有。不知何故,两种朴素的getline策略是最快的,然后是直接访问读取缓冲区稍微慢一些但仍可测量,最后是迭代器方法非常慢。我同意错误消息很重要,但其质量很难从经验上衡量。 - Sqeaky

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