使用ofstream进行缓冲文本输出以提高性能

9
我需要编写一个程序,将许多字符写入输出文件。我的程序还需要写入换行符以进行更好的格式化。我了解ofstream是一个缓冲流,如果我们使用缓冲流进行文件io,我们可以获得更好的性能。然而,如果我们使用std::endl,输出将被刷新,我们会失去由于缓冲输出而带来的任何潜在性能提升。
我想,如果我使用'\n'作为换行符,只有当我们使用std::endl时才会刷新输出。这个理解正确吗?还有哪些技巧可以用来提高文件输出的性能?
注意:我希望在完成文件写操作时刷新缓冲输出。我认为这样可以最小化文件I/O,从而提高性能。

你需要刷新文件吗?我不明白你如何既刷新文件又获得不刷新文件的性能优势。(如果你想要最佳的文件写入性能,最好选择内存映射文件。) - Mankarse
如果您担心性能问题,就不应该使用'<<'运算符。您必须坚持使用具有足够缓冲区大小的读写方法或内存映射文件(必须智能地完成)。您对'\n'和std::endl的理解是正确的 :) - Arunmu
3个回答

20

通常,如果想要最大性能,流类的用户不应该操作流的刷新:当缓冲区满时,流内部会自动刷新。这实际上比等待所有输出都准备好更有效率,特别是对于大文件:缓冲数据在仍然可能在内存中时就被写入。如果创建一个巨大的缓冲区,并且只在虚拟内存系统已经将一些数据放到磁盘上而没有放到文件中时才将其写入,则需要从磁盘读取数据并再次写入。

关于 std::endl 的主要问题是人们滥用它作为行尾标识符导致缓冲区刷新,他们不了解性能方面的影响。 std::endl 的意图是让人们在合理的时刻控制文件刷新。为了使其有效,他们需要知道自己在做什么。不幸的是,有太多人不了解 std::endl 的功能,却将其使用作为换行符,以至于它被广泛用于许多明显错误的地方。

话虽如此,以下是一些可能帮助提高性能的方法。我假设您需要格式化输出(std::ofstream::write()无法提供此功能)。

  • 显然,除非必须使用 std::endl,否则不要使用它。如果编写代码已经存在,并且在许多地方(其中一些可能超出您的控制范围)使用了 std::endl,则可以使用过滤流缓冲器,该缓冲器使用其合理大小的内部缓冲区,并且不会将对其 sync() 函数的调用转发到底层流缓冲区。尽管这涉及额外的复制,但这比一些虚假的刷新更好,因为后者的代价高得多。
  • 虽然对于 std::ofstream 应该没有影响,但是调用 std::ios_base::sync_with_stdio(false) 曾经会影响一些实现的性能。如果出现了影响,则需要考虑使用其他IOstream实现,因为可能还有其他问题影响了性能。
  • 确保使用的std::locale调用其always_noconv()时,std::codecvt<...>返回true。您可以通过使用std::use_facet<std::codecvt<char, char, stdd::mbstate_t> >(out.get_loc()).always_noconv()轻松检查此内容。您可以使用std::locale("C")获取一个应为真的std::locale
  • 某些语言环境实现使用非常低效的数字面貌实现,即使它们相当好,std::num_put<char>facet 的默认实现可能仍然会做你不需要的事情。特别是如果您的数字格式化相当简单,即您没有不断更改格式化标志,您没有替换字符映射(即您不使用有趣的std::ctype<char>facet)等,那么使用自定义std::num_put<char>facet可能是合理的:对于整数类型,创建快速但简单的格式化函数以及良好的浮点格式化函数是相当容易的,而且不会在内部使用snprintf()
  • 一些人建议使用内存映射文件,但这仅在目标文件大小已知的情况下才能合理使用。如果是这种情况,这也是改善性能的好方法,否则不值得烦恼。请注意,您可以通过创建使用内存映射接口的自定义std::streambuf来使用内存映射文件(或更一般地说,使用任何类型的输出接口进行流格式化)。我发现在使用std::istream时,内存映射有时是有效的。在许多情况下,差异并不重要。

    很久以前,我编写了自己的IOStreams和语言环境实现,它不会受到上述某些性能问题的影响(可从我的网站获取,但有点陈旧,我已经将它放置了近10年)。仍有许多可以改进的地方,但我没有最新的实现版本,可以随时发布。不过,希望很快就能有 - 这是我近10年来一直在考虑的事情...


    @Kuhl非常感谢您的详细解释。由于现在我可以控制std::endl的使用,因此我将不再使用std::endl,而是使用\n来换行。我希望这样可以提高我的性能。同时,您对我需要格式化输出的需求也非常准确。 - learningstack
    又过去了将近十年 ;) - Lightness Races in Orbit

    3

    使用\n来打印文本并不会(必须)刷新输出,但打印std::endlstd::flush则会。

    如果您希望快速写入,并且在完成后才关心数据是否存在,则使用\n进行所有写入操作,并不用担心它(因为关闭文件也会刷新流)。

    如果您仍然没有获得所需的性能,可以使用fstream::read(char*, int) -- 它允许您以任意大小的块来读取数据(尝试较大的块并查看其是否有帮助)。


    0

    是的,endl会刷新流。不要在大文件中使用它。

    还要确保设置一个流缓冲区。至少MSVC实现在没有设置缓冲区时,每次将1个字符复制到filebuf(参见streambuf::xsputn)。这可能会使您的应用程序受到CPU限制,从而导致较低的I/O速率。

    因此,在进行写入操作之前,请在您的代码中添加类似以下内容:

    char buf[256 * 1024];
    mystream.rdbuf()->pubsetbuf(buf, sizeof(buf));
    

    NB:您可以在这里找到完整的示例应用程序。


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