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

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

我们在 Windows 上的这种设置体验非常差。两个线程的总吞吐量约为 2-3 MiB/秒。驱动器似乎花费了大部分时间在两个文件之间来回查找,大概每次查找后读取的内容很少。

如果我们禁用其中一个线程并暂时查看单个线程的性能,那么我们将获得更好的带宽(对于本机而言约为 45 MiB/秒)。显然,糟糕的两线程性能是操作系统磁盘调度程序造成的。

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

一些细节:

在具有 2GiB RAM 的计算机上,每个文件的大小约为 2GiB。出于这个问题的目的,我们认为它们没有被缓存并且完美地进行了碎片整理。我们使用了碎片整理工具并重新启动以确保情况确实如此。

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

每个线程在循环中旋转,调用读取函数。我们在每次迭代中改变了 API 请求的字节数,范围从 1KiB 到 128MiB。改变这个没有任何影响,所以很明显,每次磁盘查找后操作系统物理读取的数量并不由这个数字决定。这正是我们所期望的。

单线程和两线程性能之间的显着差异在 Windows 2000、Windows XP(32 位和 64 位)、Windows Server 2003 以及使用和不使用硬件 RAID5 中都可以重复出现。

有帮助吗?

解决方案

问题似乎出在 Windows I/O 调度策略上。根据我发现的 这里 操作系统有很多方法。安排磁盘请求。虽然 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  

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

其他提示

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

我们针对 Windows 的解决方案是通过 FILE_FLAG_NO_BUFFERING 标记为 CreateFile 并在每次调用中使用大(~16MiB)读取 ReadFile. 。由于以下几个原因,这不是最理想的:

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

(作为最后的评论。这是否可以解释为什么 Windows 下的交换如此糟糕?即,Windows 无法以任何效率同时对多个文件执行 IO,因此在交换所有其他 IO 操作时被迫变得异常缓慢。)


编辑以添加威尔·迪恩的更多详细信息:

当然,在这些不同的硬件配置中,原始数据确实发生了变化(有时变化很大)。然而问题是,当从一个线程转移到两个线程时,只有 Windows 会出现性能持续下降的情况。以下是测试机器的摘要:

  • 多个不同年代的戴尔工作站(Intel Xeon)运行 Windows 2000、Windows XP(32 位)和 Windows XP(64 位),配备单驱动器。
  • 运行 Windows Server 2003(64 位)且具有 RAID 1+0 的 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 驱动器。
  • 我的家 库鲁 运行 Linux 的 PC。与其他系统相比,其性能大大不足,但我证明,在进行多线程磁盘读取时,即使这台机器也能胜过具有 RAID5 的 Windows 服务器。

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

我忘了之前提到过,但我们也尝试过普通的 Win32 CreateFile API 与 FILE_FLAG_SEQUENTIAL_SCAN 标志设置。该标志没有解决问题。

看起来确实有点奇怪,您在相当广泛的 Windows 版本中没有看到任何差异,并且在单个驱动器和硬件 raid-5 之间也没有看到任何差异。

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

您可以编写的演示此问题的最短应用程序是什么?- 我有兴趣在这里尝试一下。

我会创建某种内存线程安全锁。每个线程都可以等待锁,直到锁被释放。当锁空闲时,获取锁并读取文件指定的时间长度或指定的数据量,然后为任何其他等待线程释放锁。

你用吗 IO完成端口 在Windows下?Windows via C++ 有一个关于这个主题的深入章节,幸运的是, 也可以在 MSDN 上找到.

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

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

我对不同 API 的唯一建议是尝试内存映射文件 - 你尝试过吗?不幸的是,每个文件为 2GB,您将无法在 32 位计算机上映射多个完整文件,这意味着这并不像想象的那么简单。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top