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

时间:2020-03-05 18:39:15  来源:igfitidea点击:

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

我们有两个大文件,希望同时从两个单独的线程中读取。一个线程将顺序读取fileA,而另一个线程将顺序读取fileB。线程之间没有锁定或者通信,它们都以尽可能快的速度顺序读取,并且都立即丢弃它们读取的数据。

我们在Windows上使用此设置的经验非常差。两个线程的合并吞吐量约为2-3 MiB / sec。该驱动器似乎在大部分时间上都在两个文件之间来回搜索,大概每次搜索后读取的内容很少。

如果我们禁用其中一个线程并暂时查看单个线程的性能,那么我们将获得更好的带宽(这台机器的带宽约为45 MiB /秒)。因此很明显,糟糕的两线程性能是OS磁盘调度程序的假象。

我们有什么办法可以改善并发线程的读取性能?也许通过使用不同的API或者通过某种方式调整OS磁盘调度程序参数。

一些细节:

在具有2GiB RAM的计算机上,每个文件的顺序为2 GiB。出于这个问题的目的,我们认为它们不会被缓存和完美地进行碎片整理。我们已使用碎片整理工具并重新启动以确保是这种情况。

我们没有使用特殊的API来读取这些文件。该行为在各种沼泽标准API(例如Win32的CreateFile,C的fopen,C ++的std :: ifstream,Java的FileInputStream等)中都可以重复。

每个线程都在一个循环中旋转,从而调用read函数。我们将每次迭代从API请求的字节数从1KiB到128MiB之间的值进行了更改。改变它没有任何作用,因此很明显,此编号并不决定每次磁盘搜索后操作系统的物理读取量。这正是所期望的。

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

解决方案

回答

我想在回复中添加一些其他说明。我们测试过的所有其他非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会遭受性能的持续下降。以下是测试机器的摘要:

  • 多个不同年龄段的Dell工作站(Intel Xeon),它们使用单​​个驱动器运行Windows 2000,Windows XP(32位)和Windows XP(64位)。
  • 运行带有RAID 1 + 0的Windows Server 2003(64位)的Dell 1U服务器(Intel Xeon)。
  • 具有Windows XP(64位),Windows Server 2003和硬件RAID 5的HP工作站(AMD Opteron)。
  • 我的家用无品牌PC(AMD Athlon64),运行Windows XP(32位),FreeBSD(64位)和Linux(64位)并带有单个驱动器。
  • 我的家用MacBook(Intel Core1)运行Mac OS X,单个SATA驱动器。
  • 我的家用Koolu PC运行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位计算机上映射多个完整文件,这意味着它并不是那么简单。

回答

问题似乎出在Windows I / O调度策略中。根据我在这里找到的信息,有多种方法可以进行O.S.安排磁盘请求。尽管Linux和其他操作系统可以在不同的策略之间进行选择,但在Vista Windows被锁定为单个策略之前:FIFO队列,其中所有请求均分成64 KB的块。我相信,此策略是导致我们遇到问题的原因:调度程序将混合来自两个线程的请求,从而导致在磁盘的不同区域之间进行连续查找。
现在,好消息是,根据此处和此处的内容,Vista引入了更智能的磁盘调度程序,我们可以在其中设置请求的优先级,还可以为进程分配最小的badwidth。
坏消息是,我找不到在以前版本的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级别以某种方式影响它们,但是通过自动调整,我们将始终在每种情况下都获得最佳性能。