[英]Improving mmap memcpy file read performance
我有一个顺序从文件中读取数据的应用程序。 有些是直接从指向mmap
ed文件的指针读取的,其他部分是从文件memcpy
ed到另一个缓冲区。 我注意到在执行大量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;
}
如果底层文件和磁盘系统不够快,那么使用mmap()
或POSIX open()
/ read()
还是标准C fopen()
/ fread()
或C ++ iostream
都无关紧要。
如果性能真的很重要,并且底层文件和磁盘系统足够快, mmap()
可能是顺序读取文件的最糟糕方式。 映射页面的创建是相对昂贵的操作,并且由于每个数据字节只读取一次,因此每次实际访问的成本可能是极端的。 使用mmap()
也会增加系统的内存压力。 您可以在阅读后明确显示munmap()
页面,但是当映射被拆除时,您的处理可能会停止。
使用直接IO可能是最快的,特别是对于大文件,因为没有涉及大量的页面错误。 直接IO绕过页面缓存,这对于只读取一次数据是一件好事。 缓存数据只读一次 - 永远不会被重读 - 不仅无用,而且可能适得其反,因为CPU周期被用来驱逐页面缓存中的有用数据。
示例(为清楚起见省略了标题和错误检查):
int main( int argc, char **argv )
{
// vary this to find optimal size
// (must be a multiple of page size)
size_t copy_size = 1024UL * 1024UL;
// get a page-aligned buffer
char *buffer;
::posix_memalign( &buffer, ( size_t ) ( 4UL * 1024UL ), copy_size );
// make sure the entire buffer's virtual-to-physical mappings
// are actually done (can actually matter with large buffers and
// extremely fast IO systems)
::memset( buffer, 0, copy_size );
fd = ::open( argv[ 1 ], O_RDONLY | O_DIRECT );
for ( ;; )
{
ssize_t bytes_read = ::read( fd, buffer, copy_size );
if ( bytes_read <= 0 )
{
break;
}
}
return( 0 );
}
在Linux上使用直接IO时存在一些警告。 文件系统支持可能不稳定,直接IO的实现可能很挑剔。 您可能必须使用页面对齐的缓冲区来读取数据,如果文件不是整页,您可能无法读取文件的最后一页。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.