提高mmap memcpy文件的读取性能

时间:2018-10-16 23:41:45

标签: c++ c linux performance mmap

我有一个应用程序,可以从文件中顺序读取数据。有些是从指向mmap的文件的指针中直接读取的,而其他部分是从该文件memcpy到另一个缓冲区的。我注意到在执行所需的所有内存中的大memcpy(1MB块)时性能很差,而在执行较小的memcpy较小的调用时性能更好(在我的测试中,我使用4KB的页面大小,这花费了运行时间的1/3。)我认为,问题是使用大型memcpy时出现大量主要页面错误。

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

2 个答案:

答案 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子系统将缓存下一个读取。另外,大读取可能会被迫一起绕过缓存。