重叠I/O还是文件映射?

3

在一个Windows应用程序中,我有一个类来封装文件名和缓冲区。你可以使用文件名构造它,并且可以查询对象以查看缓冲区是否已填充,如果没有则返回nullptr,如果已经填充则返回缓冲区地址。当对象超出范围时,缓冲区会被释放:

class file_buffer
{
public:
    file_buffer(const std::string& file_name);
    ~file_buffer();
    void* buffer();

private:
    ...
}

我希望异步地将数据放入内存,据我所见,有两个选择:创建一个缓冲区并通过ReadFileEx使用重叠I/O,或者使用MapViewOfFile在另一个线程中访问地址。目前,我正在使用ReadFileEx,但存在一些问题,请求大于16MB时易发生故障:我可以尝试拆分请求,但会出现同步问题;如果IO未完成即对象已超出范围,则会出现缓冲清除问题。此外,如果快速连续创建类的多个实例,事情会变得非常棘手。映射并在另一个线程上触摸数据似乎要简单得多,因为我不会有上限问题:而且,如果客户端绝对需要立即获得数据,则可以直接引用地址,让操作系统处理页面故障并采取阻塞操作。这个应用程序需要支持单核机器,因此我的问题是:在另一个软件线程上的页面故障是否比当前线程上的重叠I/O更昂贵?它们会使进程停止吗?重叠I/O是否以同样的方式使进程停止,还是有些我不理解的操作系统魔力?页面故障是否仍然使用重叠I/O执行?我已经好好阅读了这些主题:http://msdn.microsoft.com/en-us/library/aa365199(v=vs.85).aspx(文件管理中的I/O概念)http://msdn.microsoft.com/en-us/library/windows/desktop/aa366556(v=vs.85).aspx(文件映射),但我似乎无法推断出如何进行性能权衡。

2
哇哦!我获得了飘草徽章。也许我应该添加C++标签... - hatcat
2个回答

14
您肯定希望选择内存映射文件。多年来,一些人一直推崇使用重叠I/O(使用FILE_FLAG_NO_BUFFERING)作为“将数据快速加载到RAM中的最快方法”,但这只在非常特定的情况下是真实的。在正常的平均情况下,关闭缓存会导致严重的反优化。

现在,没有FILE_FLAG_NO_BUFFERING的重叠I/O具有所有重叠I/O的怪癖,并且要慢大约50%(原因我仍然无法理解)。

我曾经进行了一些相当广泛的基准测试一年前。底线是:内存映射文件更快,更好,更可靠。

Overlapped IO使用更多的CPU,当使用缓存时速度要慢得多,异步在某些已知和未知条件下会转换为同步(例如加密、压缩和...纯粹的机会?请求大小?请求数量?),在不可预测的时间阻塞您的应用程序。有时提交请求需要“有趣”的时间,而CancelIO有时不会取消任何内容,但会等待完成。具有未完成请求的进程无法终止。管理具有未完成重叠写入的缓冲区是非常棘手的额外工作。
文件映射只需正常工作。完结撇号。它很好地运作。没有惊喜,没有有趣的东西。触摸每个页面的开销非常小,并且以磁盘能够提供的速度快速交付,并利用了缓存。您对单核CPU的担忧没有问题。如果触摸线程出现故障,则会阻塞,像往常一样,当一个线程阻塞时,另一个线程会获得CPU时间。
我现在甚至使用文件映射进行写入,每当我需要写入超过几个字节的内容时。这有点复杂(必须手动增长/预分配文件和映射,并在关闭时截断到实际长度),但是通过一些辅助类完全可以做到。写入500 MiB的数据,它需要“零时间”(你基本上执行一个memcpy,实际写入发生在后台,在任何时间之后,即使在程序完成后)。这种方法的效果非常惊人,即使你知道这是操作系统自然而然的事情。
当然,在操作系统将所有页面写出之前最好不要出现停电,但对于任何类型的写入都是如此。未在磁盘上的内容不在磁盘上 - 对此没有比这更多的解释了。如果你必须确保这一点,你必须等待磁盘同步完成,即使在等待同步的同时,你也不能确定灯光是否会熄灭这就是生活。

4
我并不比您更理解这个问题,因为似乎您已经做了一些调查。要完全确定,您需要进行实验。但这是我对这个问题的理解,倒序排列如下:
1. Windows中的文件映射和重叠IO是不同的实现,两者都使用异步块设备层,但在底层它们不依赖于彼此。我想象一下,在内核中,每个IO实际上都是异步的,但某些用户操作会等待它完成,因此它们创建了同步的幻觉。
2. 从第一个观点来看,如果一个线程执行IO操作,同一进程的其他线程将不会停止。除非系统资源稀缺或这些其他线程自己进行IO并面临某种争用。这对于第一个线程执行的任何类型的IO都是正确的:阻塞、非阻塞、重叠、内存映射。
3. 在内存映射文件中,数据至少按一页读取,可能由于预读取而读取更多,但您不能确定这一点。因此,探测线程将需要至少在每一页上触摸映射的内存。这将是类似于probe/block-probe-probe-probe-probe/block-probe...,这可能比读取几MB的大型重叠读取效率略低。或者也许内核程序员非常聪明,这会更有效率。您需要进行一些性能分析...嘿,您甚至可以不用探测线程看看会发生什么。
4. 取消重叠操作很麻烦,因此我建议使用内存映射文件。这样做要容易得多,并且您获得额外的功能: 1) 即使尚未完全在内存中,内存也是可用的 2) 内存可以/将被该进程的几个实例共享 3) 如果内存在缓存中,则准备立即完成而不仅仅是快速完成。 4) 如果数据是只读的,则可以保护内存免受写入,从而避免错误。

谢谢您的意见,它与我的看法相符。我更喜欢内存映射方法,但我已经调试、测试了代码并且销售替代品需要性能提升。 - hatcat
@hatcat:如果你还没有使用的话,可以尝试在打开文件时使用FILE_FLAG_SEQUENTIAL_SCAN。它可能会提高重叠和mmaped IO的性能。 - rodrigo

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