如何从磁盘获得良好的并发读取性能

我想问一个问题,然后用我自己的答案进行跟踪,还可以看看其他人有什么答案。

我们有两个大文件,我们想同时从两个独立的线程中读取。 一个线程将顺序读取fileA,而另一个线程将顺序读取fileB。 线程之间不存在locking或通信,两者都按顺序快速读取,并立即丢弃所读取的数据。

我们在Windows上使用这个设置的经验非常差。 两个线程的总吞吐量在2-3 MiB / sec左右。 这个驱动似乎花费了大部分时间在两个文件之间来回转移,大概在每次查找后都读得很less。

如果我们禁用其中一个线程并暂时查看单个线程的性能,那么我们将获得更好的带宽(本机约45 MiB /秒)。 所以很明显,双线程的糟糕performance是操作系统磁盘调度器的一个假象。

我们能做些什么来提高并发线程读取性能吗? 也许通过使用不同的API或以某种方式调整操作系统磁盘调度程序参数。

一些细节:

在2GiB内存的机器上,每个文件的大小都是2 GiB。 为了这个问题的目的,我们认为他们不被caching和完全碎片整理。 我们已经使用碎片整理工具,并重新启动,以确保这种情况。

我们没有使用特殊的API来读取这些文件。 这个行为可以在各种各样的bog标准API中重复使用,比如Win32的CreateFile,C的fopen,C ++的std :: ifstream,Java的FileInputStream等等。

每个线程在一个循环中旋转,调用read函数。 我们已经从1KiB到128MiB之间的值,每次迭代都要求API的字节数。 改变这一点已经没有效果了,所以在每次磁盘寻道之后操作系统在物理上读取的数量不是由这个数字决定的。 这正是应该预料的。

单线程和双线程性能之间的巨大差异在Windows 2000,Windows XP(32位和64位),Windows Server 2003以及带和不带硬件RAID5的情况下都是可重复的。

这个问题似乎在Windows I / O调度策略中。 根据我在这里发现有一个操作系统安排磁盘请求的方法很多。 虽然Linux和其他人可以在不同的策略之间进行选择,但是在Vista Windows被锁定在单个策略之前:一个FIFO队列,其中所有的请求都在64 KB的块中分割。 我相信这个策略是你遇到的问题的原因:调度程序将混合来自两个线程的请求,导致磁盘不同区域之间的连续查找。
现在,好消息是,根据这里和这里 ,Vista引入了一个更智能的磁盘调度程序,您可以在其中设置请求的优先级,并为您的进程分配最小的带宽。
坏消息是,我发现没有办法在以前的Windows版本中更改磁盘策略或缓冲区大小。 另外,即使提高进程的磁盘I / O优先级,也会提高对其他进程的性能,但仍然存在线程彼此竞争的问题。
我可以建议的是通过引入自制的磁盘访问策略来修改您的软件。
例如,您可以在线程B中使用这样的策略(类似于线程A):

if THREAD A is reading from disk then wait for THREAD A to stop reading or wait for X ms Read for X ms (or Y MB) Stop reading and check status of thread A again 

您可以使用信号来进行状态检查,或者可以使用perfmon计数器来获取实际磁盘队列的状态。 X和/或Y的值也可以通过检查实际的传输速率自动调整,并慢慢修改它们,从而使应用程序在不同的机器和/或OS上运行时的吞吐量最大化。您可以发现缓存,内存或RAID级别以某种方式影响他们,但通过自动调节,您将始终在各种情况下获得最佳性能。

我想在我的回复中再添加一些注释。 我们测试过的所有其他非Microsoft操作系统都不会遇到这个问题。 Linux,FreeBSD和Mac OS X(在不同硬件上的最后一个)在从一个线程移动到两个线程时,在总带宽方面都会更加优雅地降级。 例如,Linux从〜45 MiB / sec降低到〜42 MiB / sec。 这些其他操作系统必须在每次查找之间读取较大的文件块,因此不会花费几乎所有的时间在磁盘上等待查找。

我们的Windows解决方案是将FILE_FLAG_NO_BUFFERING标志传递给CreateFile并在每次调用ReadFile使用大量的(〜16MiB)读取。 这是不理想的,原因如下:

  • 这样读取时文件不会被缓存,所以缓存通常没有什么优势。
  • 使用此标志时的约束比正常读​​取(读取缓冲区与页面边界对齐等)复杂得多。

(作为最后一句话,这是否解释了为什么在Windows下交换是如此地狱般,也就是说,Windows无法以任何效率同时对多个文件执行IO操作,所以在交换所有其他IO操作时,被迫的速度太慢了。


编辑为Will Dean添加一些更多细节:

当然,在这些不同的硬件配置中,原始的数字确实发生了改变 然而,问题是性能的一致性下降,只有Windows在从一个线程移动到两个线程时受到影响。 以下是测试机器的总结:

  • 几台运行Windows 2000,Windows XP(32位)和Windows XP(64位)的单个驱动器的戴尔工作站(Intel Xeon)。
  • 运行带有RAID 1 + 0的Windows server 2003(64位)的Dell 1U服务器(Intel Xeon)。
  • 采用Windows XP(64位)和Windows server 2003以及硬件RAID 5的HP工作站(AMD Opteron)。
  • 运行Windows XP(32位),FreeBSD(64位)和Linux(64位)的单一驱动器的家庭非品牌PC(AMD Athlon64)。
  • 我家的MacBook(Intel Core1)运行Mac OS X,单个SATA驱动器。
  • 我的家庭Koolu个人电脑运行Linux。 与其他系统相比,效率很低,但是我证明,即使是在多线程磁盘读取的情况下,这台机器也可以超越具有RAID5的Windows服务器。

所有这些系统的CPU使用率在测试期间都非常低,反病毒功能被禁用。

我忘了之前提到,但我们也尝试了FILE_FLAG_SEQUENTIAL_SCAN标志设置正常的Win32的CreateFile API。 这个标志没有解决这个问题。

看起来有点奇怪,你看到相当广泛的Windows版本没有区别,单个驱动器和硬件raid-5之间没有任何区别。

这只是“直觉”,但这确实使我怀疑这确实是一个简单的寻求问题。 除了OS X和Raid5之外,所有这些都是在同一台机器上进行的 – 你试过另一台机器吗? 在这个测试中你的CPU使用率基本上是零吗?

什么是最短的应用程序,你可以写这证明了这个问题? – 我有兴趣在这里尝试。

我会创建一些内存线程安全锁。 每个线程可以等待锁定,直到它是空闲的。 当锁变为空闲时,取锁并读取文件一段定义的时间或定义的数据量,然后释放其他正在等待的线程的锁。

你在Windows下使用IOCompletionPorts吗? 通过C ++的Windows有一个关于这个主题的深入篇章,幸运的是, 它也可以在MSDN上得到 。

保罗 – 看到了更新。 很有意思。

在Vista或Win2008上试用它会很有趣,因为在某些情况下,人们似乎正在报告一些相当大的I / O改进。

我唯一的关于不同的API的建议是尝试内存映射文件 – 你试过吗? 不幸的是,在每个文件2GB的情况下,你不能在一台32位的机器上映射多个整个文件,这意味着这并不是那么微不足道。