27

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

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

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

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

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

一些细节:

在具有 2GiB 内存的机器上,这些文件的大小约为 2GiB。出于这个问题的目的,我们认为它们没有被缓存和完美碎片整理。我们使用了碎片整理工具并重新启动以确保是这种情况。

我们没有使用特殊的 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 的情况下,单线程和双线程性能之间的巨大差异是可以重复的。

4

6 回答 6

12

问题似乎出在 Windows I/O 调度策略中。根据我在这里发现的内容,操作系统可以通过多种方式安排磁盘请求。虽然 Linux 和其他人可以在不同的策略之间进行选择,但在 Vista 之前,Windows 被锁定在一个策略中:一个 FIFO 队列,其中所有请求都分成 64 KB 块。我相信这个策略是你遇到的问题的原因:调度程序会混合来自两个线程的请求,导致磁盘不同区域之间的连续寻道。
现在,好消息是,根据herehere,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 级别以某种方式影响它们,但是通过自动调整,您将始终在每种情况下获得最佳性能。

于 2008-08-13T13:47:39.333 回答
6

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

我们针对 Windows 的解决方案是将FILE_FLAG_NO_BUFFERING标志传递给CreateFile并在每次调用ReadFile. 由于以下几个原因,这是次优的:

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

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


编辑为 Will Dean 添加更多详细信息:

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

  • 运行 Windows 2000、Windows XP(32 位)和 Windows XP(64 位)的多个不同年代的戴尔工作站(英特尔至强),带有单个驱动器。
  • 运行 Windows Server 2003(64 位)和 RAID 1+0 的戴尔 1U 服务器(英特尔至强)。
  • 具有 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 的Koolu PC。与其他系统相比,动力明显不足,但我证明即使这台机器在进行多线程磁盘读取时也可以胜过具有 RAID5 的 Windows 服务器。

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

我之前忘了提,但我们也尝试了设置标志的普通 Win32 CreateFileAPI 。FILE_FLAG_SEQUENTIAL_SCAN这个标志没有解决问题。

于 2008-08-12T19:51:06.323 回答
1

确实有点奇怪,您发现在相当广泛的 Windows 版本之间没有任何区别,并且在单个驱动器和硬件 raid-5 之间没有任何区别。

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

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

于 2008-08-12T20:20:17.217 回答
0

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

于 2008-08-12T20:04:13.550 回答
0

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

于 2008-08-12T21:38:05.017 回答
0

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

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

我对不同 API 的唯一建议是尝试对文件进行内存映射——你试过了吗?不幸的是,在每个文件 2GB 的情况下,您将无法在 32 位机器上映射多个整个文件,这意味着这并不像它可能的那么简单。

于 2008-08-13T09:44:26.017 回答