C++中最快的高性能顺序文件I/O方法是什么?

51

假设以下内容...
输出:
文件已打开...
数据被“流式”写入磁盘。内存中的数据位于一个大的连续缓冲区中。它是以其原始格式直接从该缓冲区写入磁盘的。缓冲区的大小可配置,但在流的持续时间内固定。缓冲区依次写入文件。不进行任何搜索操作。
...文件已关闭。

输入:
从头到尾读取一个大文件(按上述方式顺序写入)。


C ++中实现最快速顺序文件I / O的通常接受的准则是否存在?

一些可能的考虑因素:

  • 选择最佳缓冲区大小的准则
  • 像boost :: asio这样的可移植库是否过于抽象,无法暴露特定平台的复杂性,或者可以假定它们是最佳的?
  • 异步I / O是否总是优于同步I / O?如果应用程序没有CPU限制会怎样?

我知道这将考虑到特定的平台。 我欢迎通用准则,以及针对特定平台的评论。
(我最紧急的兴趣在Win x64上,但我还对Solaris和Linux的评论感兴趣)


你想重新实现 cp 命令吗?我觉得我漏掉了什么... - Steve Schnepp
7个回答

33
在C++中,是否有达到最快顺序文件I/O的通用准则?
规则0:测量。使用所有可用的性能分析工具并熟悉它们。这是编程中的一条戒律,如果你没有测量它,你就不知道它有多快,对于I/O来说更是如此。如果可能的话,请确保在实际工作条件下进行测试。一个没有竞争的I/O系统的进程可以被过度优化,调整为在真实负载下不存在的条件。
  1. 使用映射内存而不是写入文件。这并不总是更快,但它提供了通过避免不必要的复制和利用操作系统对磁盘实际使用方式的了解,在特定于操作系统但相对可移植的方式上进行I/O优化的机会(如果您使用包装器而不是特定于操作系统的API调用,则“可移植”)。
  2. 尽可能线性化输出。在优化条件下,必须跳转到内存以查找缓冲区可能会产生明显的影响,因为缓存行、分页和其他内存子系统问题将开始变得重要。如果有很多缓冲区,请查看支持scatter-gather I/O的支持,该功能试图为您完成线性化。
一些可能的考虑:
  • 选择最佳缓冲区大小的准则
以页面大小为起点,但要准备进行调整。
  • 一个像boost::asio这样的可移植库是否过于抽象以至于无法暴露特定平台的复杂性,还是可以假定它们是最优的?

不要假设它是最优的。这取决于库在您的平台上得到多少彻底的使用,以及开发人员付出了多少努力使其快速。话虽如此,一个可移植的I/O库可以非常快,因为大多数系统都存在快速抽象,并且通常可以想出一个通用的API来涵盖许多基础功能。据我有限的了解,Boost.Asio相当针对其所在的特定平台进行了精细调整:有一个专门用于快速异步I/O的整个OS和OS变体特定的API系列(例如epoll/dev/epollkqueueWindows overlapped I/O),而Asio将它们全部封装起来。

  • 异步I/O总是优于同步I/O吗?如果应用程序不受CPU限制,会怎么样?

异步 I/O 并不比同步 I/O 更快。异步 I/O 确保你的代码不会浪费时间等待 I/O 完成。它比使用线程这种不浪费时间的方法更加高效,因为当 I/O 准备就绪时,它会回调到你的代码,而不是在此之前。没有错误的启动或需要终止空闲线程的担忧。


13

一般建议关闭缓冲并使用大块读写(但不要太大,否则您将浪费太多等待整个I/O完成的时间,否则您可以开始消耗第一个兆字节。这个算法很容易找到甜点,只有一个旋钮可以调整:块大小)。

除此之外,对于输入,mmap()将文件共享并以只读方式读取是(如果不是最快的话)最有效的方式。如果你的平台有,就调用madvise(),告诉内核你将如何遍历文件,这样它就可以进行预读和快速丢弃页面。

对于输出,如果您已经有一个缓冲区,请考虑使用文件作为其基础(也使用mmap()),这样您就不必在用户空间中复制数据。

如果您不喜欢mmap(),那么就有fadvise(),对于真正困难的情况,则有异步文件I/O。

(以上全部是POSIX,Windows名称可能有所不同)。


1
修复:fadvise(2)和madvise(2)。此外,posix版本的名称为posix_fadvise和posix_madvise。 - osgx
mmap()输出文件如何避免在用户空间中复制数据? - Paul Merrill
@PaulMerrill 我认为这是因为它是零拷贝。在用户空间和内核空间之间不需要进行任何复制。 - Bruce Adams

9

对于Windows系统,如果你选择使用针对该平台的API调用CreateFile(),请确保在函数调用中使用FILE_FLAG_SEQUENTIAL_SCAN参数,以优化I/O缓存。至于缓冲区大小,通常建议使用磁盘扇区大小的倍数。8K是一个不错的起点,增加缓冲区大小很少有收益。

本文讨论了Windows上异步和同步操作之间的比较。

http://msdn.microsoft.com/en-us/library/aa365683(VS.85).aspx


3

正如您上面所指出的,一切都取决于您使用的机器/系统/库。在一个系统上快速解决方案可能在另一个系统上变得缓慢。

然而,一般的指导原则是尽可能地写大块内容。
通常,逐字节编写是最慢的。

确切知道哪种方式最好的方法是编写几种不同的代码,并对它们进行分析。


2
在Linux上,缓冲区读写速度非常快,随着缓冲区大小的增加而逐渐提高,但收益递减,通常应使用BUFSIZ(由stdio.h定义)作为较大缓冲区大小不会有太多帮助。 mmap提供了最快的文件访问,但mmap调用本身相当昂贵。对于小文件(16KiB),readwrite系统调用胜出(请参见https://dev59.com/I5bfa4cB1Zd3GeqP1Pha#39196499,了解通过readmmap进行读取的数字)。

2
您询问了关于C++的问题,但是看起来您已经超越了这个阶段,准备进一步了解特定于平台的内容。
在Windows上,使用文件映射和FILE_FLAG_SEQUENTIAL_SCAN可能是最快的方法。实际上,在文件实际写入磁盘之前,您的进程可以退出。如果没有明确阻塞刷新操作,Windows可能需要最多5分钟才能开始写入这些页面。
如果文件不在本地设备而是网络驱动器上,则需要小心。网络错误将显示为SEH错误,您需要准备好处理它们。
在*nix系统上,将数据按顺序写入原始磁盘设备可能会获得更高的性能。在Windows上也可以实现,但API的支持不如*nix系统好。这将避免一些文件系统开销,但可能并不足够有用。
粗略地说,RAM比磁盘快1000倍或更多,CPU的速度更快。除了尽可能避免磁头移动(寻道)之外,可能没有太多的逻辑优化能够帮助。专门为此文件使用的磁盘可以在这里大有裨益。

1
POSIX具有与POSIX_FADV_SEQUENTIAL标志兼容的posix_fadvise调用。 - osgx

2
你可以使用CreateFileReadFile来获得绝对最快的性能。打开文件时,请使用FILE_FLAG_SEQUENTIAL_SCAN
使用2的幂作为缓冲区大小进行读取。只有基准测试才能确定这个数字。我曾经看到过它是8K,还有一次发现它是8M!这种情况差异很大。
这取决于CPU缓存的大小,操作系统预读的效率以及执行许多小写操作所涉及的开销。
内存映射不是最快的方法。它具有更多开销,因为你无法控制块大小,操作系统需要装入所有页面。

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