内存映射 i/o 值得顺序处理吗?
Is memory mapped i/o worthwhile for sequential processing?
考虑一个程序,它将在一次处理中按顺序处理整个输入文件。将文件映射到内存与将其读入缓冲区进行处理相比有什么优势吗?
我知道如果您打算只访问文件的一部分,那么内存映射 i/o 可以节省对文件不需要的部分的磁盘访问。但我对整个文件的一次连续传递感兴趣。
如果您要多次读取文件(或至少部分文件),让虚拟内存系统确定将哪些部分保留在缓存中可能会更快。但是,同样,对整个文件的一次连续传递不会从中受益。
我知道高级 i/o(例如,C++ i/o 流或像 fscanf 这样的 C 函数)在 OS 的基本读取操作。让我们避开语言的标准库并关注 OS 调用(即 Windows 上的 ReadFile
或 Linux 上的 read()
)。
在我看来,这两种方法的瓶颈(从光盘读取数据)都是一样的,但我听说有人声称内存映射的开销较小,即使在对整个文件进行一次顺序传递的情况下也是如此.
我承认,如果两个程序试图通过内存映射读取同一个文件,那么第二个程序可以将相同的物理页面映射到它自己的地址 space,从而避免实际的磁盘读取。还有其他优势吗?
我主要对 Windows 感兴趣,但如果您也能指出与 Linux 的任何显着差异,则会加分。
有可能。与使用 MMF 相比,不必发出尽可能多的系统调用来读取文件,可能会获得较小的性能提升。
根据您的连续评论,我假设您的程序是单线程的。如果您正在执行 CPU 密集处理,您可以告诉内核在后台预取文件(使用 PrefetchVirtualMemory
),同时开始处理它的开头。这比处理它的一部分并在循环中调用 ReadFile
更高效,因为您不必等待 ReadFile
到 return,也不必等待读取整个文件在开始处理之前记住自己。尽管我认为您可以使用异步 IO 自行组合一些类似的东西,但是既然 OS 可以为您做到这一点,为什么还要重新发明轮子呢。
我 运行 通过修改读取文本文件并将单词放入 TRIE 的程序在 Windows 上进行实验。为了专注于 i/o 性能,我注释掉了实际的 TRIE 操作,因此程序只是读取文本并将其分解为单词。
结果
Method
C++ iostreams 228 ms (σ = 6)
Win32 ReadFile 115 ms (σ = 8)
memory mapped 136 ms (σ = 14)
结论
结果证实了我的怀疑,对于单次顺序传递,内存映射相对于 Win32 ReadFile 没有实质性优势。事实上,可能会有一个小的惩罚(更多的系统调用?)和更多的变化。
需要说明的是,这只是在一台 Windows 机器上进行的测试。我听说过 Linux 上的 mmap 可能更快的合理解释。
毫不奇怪,C++ iostreams 库中的额外缓冲层使其成为最慢的方法。
方法
对于输入,我使用了 Project Guttenberg 中的 18 本书,总计 10,558,803 字节。这些书主要是 ASCII,但有些包含一些编码为 UTF-8 的非 ASCII 字符。
主程序循环打开一个文件,一次性将整个文件读入(或映射)到内存中,对其进行标记,然后关闭(或取消映射)该文件。
标记化是一个手写状态机,为每个单词构建一个 std::string_view
。它按顺序读取每个字节一次。我保留了标记化以确保文件读取解决方案与内存映射解决方案之间的同类比较,否则可能不会将数据带入内存。
用 C++ 编码,使用 /EHsc /O2 /std:c++latest
使用 MSVC 2019 编译成 64 位可执行文件。在带有 SSD 的基于 Intel 的台式机上执行。
每个实验 运行 七次使用热缓存。时间用 std::chrono::high_resolution_clock
记录并以毫秒为单位报告。无论方法如何,分词器都报告了在每个 运行 上读取的相同字节和找到的单词。
C++ iostreams 方法:
文件以二进制模式打开,因此不会浪费精力 运行将 CR+LF 改为 '\n'
。我们一次性将每个文件读入std::string
。
auto file = std::ifstream(file_name, std::ios::binary);
std::string text{std::istreambuf_iterator(file), {}};
file.close();
Tokenize(text);
Win32 读取文件方法:
请注意,我们使用 FILE_FLAG_SEQUENTIAL_SCAN
进行了提示,并且没有错误检查。我们对每个文件使用了一个 ReadFile 调用,将数据放入一个 std::string
中,该 std::string
使用文件大小进行了预分配和零初始化。
内存映射方法
我们使用相同的选项打开文件(特别是 FILE_FLAG_SEQUENTIAL_SCAN
。与 ReadFile 方法相比,还有额外的系统调用(CreateFileMapping、MapViewOfFiew、UnMapViewOfFile 和额外的 CloseHandle)。
考虑一个程序,它将在一次处理中按顺序处理整个输入文件。将文件映射到内存与将其读入缓冲区进行处理相比有什么优势吗?
我知道如果您打算只访问文件的一部分,那么内存映射 i/o 可以节省对文件不需要的部分的磁盘访问。但我对整个文件的一次连续传递感兴趣。
如果您要多次读取文件(或至少部分文件),让虚拟内存系统确定将哪些部分保留在缓存中可能会更快。但是,同样,对整个文件的一次连续传递不会从中受益。
我知道高级 i/o(例如,C++ i/o 流或像 fscanf 这样的 C 函数)在 OS 的基本读取操作。让我们避开语言的标准库并关注 OS 调用(即 Windows 上的
ReadFile
或 Linux 上的read()
)。
在我看来,这两种方法的瓶颈(从光盘读取数据)都是一样的,但我听说有人声称内存映射的开销较小,即使在对整个文件进行一次顺序传递的情况下也是如此.
我承认,如果两个程序试图通过内存映射读取同一个文件,那么第二个程序可以将相同的物理页面映射到它自己的地址 space,从而避免实际的磁盘读取。还有其他优势吗?
我主要对 Windows 感兴趣,但如果您也能指出与 Linux 的任何显着差异,则会加分。
有可能。与使用 MMF 相比,不必发出尽可能多的系统调用来读取文件,可能会获得较小的性能提升。
根据您的连续评论,我假设您的程序是单线程的。如果您正在执行 CPU 密集处理,您可以告诉内核在后台预取文件(使用 PrefetchVirtualMemory
),同时开始处理它的开头。这比处理它的一部分并在循环中调用 ReadFile
更高效,因为您不必等待 ReadFile
到 return,也不必等待读取整个文件在开始处理之前记住自己。尽管我认为您可以使用异步 IO 自行组合一些类似的东西,但是既然 OS 可以为您做到这一点,为什么还要重新发明轮子呢。
我 运行 通过修改读取文本文件并将单词放入 TRIE 的程序在 Windows 上进行实验。为了专注于 i/o 性能,我注释掉了实际的 TRIE 操作,因此程序只是读取文本并将其分解为单词。
结果
Method
C++ iostreams 228 ms (σ = 6)
Win32 ReadFile 115 ms (σ = 8)
memory mapped 136 ms (σ = 14)
结论
结果证实了我的怀疑,对于单次顺序传递,内存映射相对于 Win32 ReadFile 没有实质性优势。事实上,可能会有一个小的惩罚(更多的系统调用?)和更多的变化。
需要说明的是,这只是在一台 Windows 机器上进行的测试。我听说过 Linux 上的 mmap 可能更快的合理解释。
毫不奇怪,C++ iostreams 库中的额外缓冲层使其成为最慢的方法。
方法
对于输入,我使用了 Project Guttenberg 中的 18 本书,总计 10,558,803 字节。这些书主要是 ASCII,但有些包含一些编码为 UTF-8 的非 ASCII 字符。
主程序循环打开一个文件,一次性将整个文件读入(或映射)到内存中,对其进行标记,然后关闭(或取消映射)该文件。
标记化是一个手写状态机,为每个单词构建一个 std::string_view
。它按顺序读取每个字节一次。我保留了标记化以确保文件读取解决方案与内存映射解决方案之间的同类比较,否则可能不会将数据带入内存。
用 C++ 编码,使用 /EHsc /O2 /std:c++latest
使用 MSVC 2019 编译成 64 位可执行文件。在带有 SSD 的基于 Intel 的台式机上执行。
每个实验 运行 七次使用热缓存。时间用 std::chrono::high_resolution_clock
记录并以毫秒为单位报告。无论方法如何,分词器都报告了在每个 运行 上读取的相同字节和找到的单词。
C++ iostreams 方法:
文件以二进制模式打开,因此不会浪费精力 运行将 CR+LF 改为 '\n'
。我们一次性将每个文件读入std::string
。
auto file = std::ifstream(file_name, std::ios::binary);
std::string text{std::istreambuf_iterator(file), {}};
file.close();
Tokenize(text);
Win32 读取文件方法:
请注意,我们使用 FILE_FLAG_SEQUENTIAL_SCAN
进行了提示,并且没有错误检查。我们对每个文件使用了一个 ReadFile 调用,将数据放入一个 std::string
中,该 std::string
使用文件大小进行了预分配和零初始化。
内存映射方法
我们使用相同的选项打开文件(特别是 FILE_FLAG_SEQUENTIAL_SCAN
。与 ReadFile 方法相比,还有额外的系统调用(CreateFileMapping、MapViewOfFiew、UnMapViewOfFile 和额外的 CloseHandle)。