提高mmap memcpy文件读取性能

14

我有一个应用程序,从文件中顺序读取数据。一些数据直接从映射文件的指针读取,另一些数据从文件复制到另一个缓冲区。当进行需要大量内存(1MB块)的大型memcpy时,性能表现不佳,而进行许多较小的memcpy调用时,性能表现更好(在我的测试中,我使用了4KB,即页面大小,运行时间为大块的1/3)。我认为问题是在进行大型memcpy时出现了大量的主要页面错误。

我尝试了各种调整参数(MAP_POPULATEMADV_WILLNEEDMADV_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;
}

1
你在这些测试中考虑了文件缓存吗? - that other guy
2
另外,当我打开优化构建时,由于没有可观察到的更改结果,memcpy被完全优化掉了--https://godbolt.org/z/zGdG1w。你使用什么编译器和标志来构建这个?在memcpy之后添加一些愚蠢的东西会把它放回去,例如:volatile uint8_t c = buffer[10]; https://godbolt.org/z/ctTRPV - xaxxon
memcpy使用不同的算法取决于大小。较大的复制将避免使用L2/L3缓存。也许这就是差异的原因。顺便说一下,如果你要复制大量数据,不应该使用mmap。应该使用“read”代替。 - geza
@geza 这可能是问题所在,我不太确定。这个例子比真实代码简单得多,只是展示了相同的问题。使用mmap使处理其他逻辑更简单。我还尝试过使用向量IO而不是mmap的版本(真实应用程序处理每个块的“头”结构和“有效负载”,有效负载必须复制到特定位置,而头只需要被应用程序读取),它的性能与4KB mmap复制版本类似,但由于处理向量I/O的部分读取而使代码变得难以阅读。 - Alex
@aegfault (a) 64GB 的内存——在我进行这些测试时没有太多东西在运行。(b) 它从未进入交换空间。(c) 感谢您的建议,但那并没有什么区别。我很惊讶大多数标志似乎会使事情变得更糟——特别是 MADV_SEQUENTIAL,它使其爬行,尽管这非常顺序化。我曾经读过使用 MADV_SEQUENTIAL 和 MAP_POPULATE 一起会提示预读,我认为这会有所帮助。 - Alex
显示剩余16条评论
1个回答

1
如果底层文件和磁盘系统不够快,无论您使用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的实现可能会很棘手。您可能需要使用页面对齐的缓冲区来读取数据,并且如果最后一页不是完整页,则可能无法读取文件的最后一页。


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