我有一个应用程序,可以从文件中顺序读取数据。有些是从指向mmap
的文件的指针中直接读取的,而其他部分是从该文件memcpy
到另一个缓冲区的。我注意到在执行所需的所有内存中的大memcpy
(1MB块)时性能很差,而在执行较小的memcpy
较小的调用时性能更好(在我的测试中,我使用4KB的页面大小,这花费了运行时间的1/3。)我认为,问题是使用大型memcpy
时出现大量主要页面错误。
我尝试了各种调整参数(MAP_POPUATE
,MADV_WILLNEED
,MADV_SEQUENTIAL
),但并没有明显改善。
我不确定为什么许多小的memcpy
通话会更快?似乎违反直觉。有什么办法可以改善这一点?
结果和测试代码如下。
在CentOS 7(Linux 3.10.0),默认编译器(gcc 4.8.5)上运行,从常规磁盘的RAID阵列读取29GB文件。
以/usr/bin/time -v
运行:
4KB memcpy
:
User time (seconds): 5.43
System time (seconds): 10.18
Percent of CPU this job got: 75%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:20.59
Major (requiring I/O) page faults: 4607
Minor (reclaiming a frame) page faults: 7603470
Voluntary context switches: 61840
Involuntary context switches: 59
1MB memcpy
:
User time (seconds): 6.75
System time (seconds): 8.39
Percent of CPU this job got: 23%
Elapsed (wall clock) time (h:mm:ss or m:ss): 1:03.71
Major (requiring I/O) page faults: 302965
Minor (reclaiming a frame) page faults: 7305366
Voluntary context switches: 302975
Involuntary context switches: 96
MADV_WILLNEED
似乎对1MB复制结果没有太大影响。
MADV_SEQUENTIAL
大大降低了1MB复制结果的速度,我没有等待它完成(至少7分钟)。
MAP_POPULATE
将1MB复制结果的速度降低了大约15秒。
用于测试的简化代码:
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
int
main(int argc, char *argv[])
{
try {
char *filename = argv[1];
int fd = open(filename, O_RDONLY);
if (fd == -1) {
throw std::runtime_error("Failed open()");
}
off_t file_length = lseek(fd, 0, SEEK_END);
if (file_length == (off_t)-1) {
throw std::runtime_error("Failed lseek()");
}
int mmap_flags = MAP_PRIVATE;
#ifdef WITH_MAP_POPULATE
mmap_flags |= MAP_POPULATE; // Small performance degredation if enabled
#endif
void *map = mmap(NULL, file_length, PROT_READ, mmap_flags, fd, 0);
if (map == MAP_FAILED) {
throw std::runtime_error("Failed mmap()");
}
#ifdef WITH_MADV_WILLNEED
madvise(map, file_length, MADV_WILLNEED); // No difference in performance if enabled
#endif
#ifdef WITH_MADV_SEQUENTIAL
madvise(map, file_length, MADV_SEQUENTIAL); // Massive performance degredation if enabled
#endif
const uint8_t *file_map_i = static_cast<const uint8_t *>(map);
const uint8_t *file_map_end = file_map_i + file_length;
size_t memcpy_size = MEMCPY_SIZE;
uint8_t *buffer = new uint8_t[memcpy_size];
while (file_map_i != file_map_end) {
size_t this_memcpy_size = std::min(memcpy_size, static_cast<std::size_t>(file_map_end - file_map_i));
memcpy(buffer, file_map_i, this_memcpy_size);
file_map_i += this_memcpy_size;
}
}
catch (const std::exception &e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
答案 0 :(得分:1)
如果基础文件和磁盘系统不够快,则无论您使用mmap()
还是POSIX open()
/ read()
还是标准C fopen()
/ {{1} }或C ++ fread()
无关紧要。
但是,如果性能确实很重要并且基础文件和磁盘系统足够快,那么iostream
可能是顺序读取文件的最糟糕的方法。映射页面的创建是一个相对昂贵的操作,并且由于仅读取一次数据的每个字节,因此每次实际访问的成本可能非常高。使用mmap()
也会增加系统的内存压力。阅读页面后,您可以显式mmap()
页,但是当映射被拆除时,处理可能会停顿。
使用直接IO可能是最快的,特别是对于大型文件,因为其中没有大量的页面错误。 Direct IO绕过页面缓存,这对于仅读取一次数据是一件好事。缓存一次只能读取一次的数据-永不重复读取-不仅无用,而且由于CPU周期被用来从页面缓存中删除有用数据而可能适得其反。
示例(为清楚起见,省略了标题和错误检查):
munmap()
在Linux上使用直接IO时,存在一些警告。文件系统支持可能参差不齐,直接IO的实现可能很复杂。您可能必须使用页面对齐的缓冲区来读取数据,并且如果它不是整页,则可能无法读取文件的最后一页。
答案 1 :(得分:0)
我认为重要的是CPU利用率。在4KB版本中,您的代码受计算限制,这意味着它将以处理器允许的速度运行。在1MB版本中,您的代码受I / O约束,这意味着它将以I / O子系统所允许的速度运行。我会尝试以4KB的增量增加大小,直到找到交叉点。您可能会在此时看到最佳性能。还要注意,由于I / O的速度比CPU慢得多,因此I / O子系统通常具有可以利用的缓存。因此,当您处理较小的缓冲区时,I / O子系统将缓存下一个读取。另外,大读取可能会被迫一起绕过缓存。