使用mmap在写入连续日志文件时是否能提高速度?

4
我想写日志文件,使用 mmap(以提高速度)的非结构化格式(每次一行)。最佳流程是什么?我应该打开空文件,truncate 到 1 页大小(写入空字符串以调整文件大小?),然后 mmap - 当 mmap 区域已满时重复此过程吗?
通常,我使用 mmap 写入固定大小的结构,通常只有一页,但这是用于使用 mmap 写入日志文件(从 0.5 到 10 Gb 不等),但不确定第一个 mmap 区域填满后的最佳做法- munmap,调整文件大小 truncatemmap 下一页?
在将日志写入内存区域时,我会跟踪大小和 msync,当到达映射内存区域的末尾时,如何正确处理?
假设我永远不需要返回或覆盖现有数据,因此我只向文件中写入新数据。
问题1:当到达映射区域的末尾时,我是否需要 munmap,将文件 ftruncate 调整为另一页大小并 mmap 下一页?
问题2:是否有标准方法来预先准备下一页以便进行下一次写入?当我们接近映射区域的末尾时,在另一个线程上执行此操作?
问题3:我需要为顺序访问使用 madvise 吗?
这是实时数据处理,需要保留日志文件-目前我只写入文件。日志文件是非结构化的,文本格式,基于行。
这适用于 linux/c++/c,在 Mac 上进行测试(因此没有 remap [?])。
感谢任何关于最佳实践的链接/指针。
2个回答

22
我写了一个关于fwrite和mmap的比较的本科论文(“一项实验,以测量传统I/O和内存映射文件之间的性能折衷”)。首先,对于写入操作,你不必使用内存映射文件,特别是对于大文件。fwrite是完全可以的,并且几乎总是优于使用mmap的方法。mmap将为并行数据读取提供最大的性能提升;对于顺序数据写入,你真正受到的限制是硬件。
在我的示例中,remapSize是文件的初始大小,并且文件每次重新映射时增加的大小。 fileSize跟踪文件的大小,mappedSpace表示当前mmap的大小(长度),alreadyWrittenBytes是已经写入文件的字节数。
下面是示例初始化代码:
void init() {
  fileDescriptor = open(outputPath, O_RDWR | O_CREAT | O_TRUNC, (mode_t) 0600); // Open file
  result = ftruncate(fileDescriptor, remapSize); // Init size
  fsync(fileDescriptor); // Flush
  memoryMappedFile = (char*) mmap64(0, remapSize, PROT_WRITE, MAP_SHARED, fileDescriptor, 0); // Create mmap
  fileSize = remapSize; // Store mapped size
  mappedSpace = remapSize; // Store mapped size
}

广告问题1:

我使用了一个“取消映射-重映射”机制。

取消映射

  • 首先刷新(msync)
  • 然后取消映射内存映射文件。

这可能看起来像下面这样:

void unmap() {
  msync(memoryMappedFile, mappedSpace, MS_SYNC); // Flush
  munmap(memoryMappedFile, mappedSpace)
}

对于Remap,您可以选择重新映射整个文件或仅重新映射新增的部分。

Remap基本上会:

  • 增加文件大小
  • 创建新的内存映射

重新映射整个文件的示例实现:

void fullRemap() {
  ftruncate(fileDescriptor, mappedSpace + remapSize); // Make file bigger
  fsync(fileDescriptor); // Flush file
  memoryMappedFile = (char*) mmap64(0, mappedSpace + remapSize, PROT_WRITE, MAP_SHARED, fileDescriptor, 0); // Create new mapping on the bigger file
  fileSize += reampSize;
  mappedSpace += remapSize; // Set mappedSpace to new size
}

小型remap的示例实现:

void smallRemap() {
  ftruncate(fileDescriptor, fileSize + remapSize); // Make file bigger
  fsync(fileDescriptor); // Flush file
  remapAt = alreadyWrittenBytes % pageSize == 0 
            ? alreadyWrittenBytes 
            : alreadyWrittenBytes - (alreadyWrittenBytes % pageSize); // Adjust remap location to pagesize
  memoryMappedFile = (char*) mmap64(0, fileSize + remapSize - remapAt, PROT_WRITE, MAP_SHARED, fileDescriptor, remapAt); // Create memory-map
  fileSize += remapSize;
  mappedSpace = fileSize - remapAt;
}

有一个名为mremap函数的函数存在,但它声明:

该调用是特定于Linux的,不应在旨在可移植的程序中使用。

关于问题二:

我不确定我是否正确理解了这一点。如果您想要告诉内核“现在加载下一页”,那么不,这是不可能的(至少据我所知)。但请参阅问题3以了解如何向内核提供建议。

关于问题三:

您可以使用带有标志MADV_SEQUENTIALmadvise,但请记住,这并不强制内核预读,而仅仅是给出建议。

来自man的摘录:

可能会导致内核大量预读

个人结论:

不要使用mmap进行顺序数据写入。这只会导致更多的开销,并且会比使用fwrite的简单写入算法产生更多“不自然”的代码。

对于大文件的随机访问读取,请使用mmap

这也是我在我的论文中获得的结果。我无法通过使用mmap进行顺序写入来实现任何加速,事实上,它总是比这个目的慢。


mmap确实没有中间缓冲区,并直接按请求写入数据(write等也是如此),这是用户空间API通常避免的。这也允许更少的系统调用,而系统调用的开销很大。 - edmz
1
你能提供mmap与fwrite以及write的基准数据吗? - Andrew Henle
1
我只是将mmapfwrite进行了比较,包括并行化和侧载等进一步参数。然而,论文目前尚未完全完成和发表,因此我不确定是否可以在此时发布结果。 - Markus Weninger
Markus,一段时间过去了。你现在可以分享你的结果了吗?我特别想知道fwrite为什么更快,而mmap为什么更慢。我的猜测是我从未使用过msync()。我只是mmap(),写入数据,munmap(),然后数据会在自己的时间内出现在磁盘上。fwrite()是否也通过不执行msync()或类似操作来提高速度? - Swiss Frank
使用mmap的原因可能不仅仅是IO速度。例如,如果您的(可能是多线程的)程序在写出时崩溃,则会丢失日志消息。如果OOM杀死了您的进程,则会丢失日志消息。如果您忘记使用追加模式,则整个文件可能会损坏。此外,您必须进行同步操作。使用内存映射,您无需担心这些问题(您示例中的许多同步操作是不需要的)。您有一个“某些内存区域”供您编写,之后发生的事情就不再是您的问题了。当然,页面可能不会立即被写出,但这没关系(甚至是可取的)。 - Damon

8
使用mmap(以提高速度)。最佳步骤是什么?
不要使用mmap,使用write。说真的,为什么人们总是认为mmap会以某种方式神奇地加快速度呢?
创建mmap并不便宜,这些页表不会自己填充。当您想要追加到文件时,您必须:
- 截断为新大小(对于现代文件系统来说,这实际上相当便宜) - 取消映射旧映射(留下可能需要写出的脏页面) - 映射新映射,这需要填充页表。每次写入以前未故障的页面时,都会调用页面错误处理程序。
mmap有一些很好的用途,例如在大型数据集中进行随机访问读取或从同一数据集进行反复读取时。
有关详细信息,我将引用Linus Torvalds本人:

http://lkml.iu.edu/hypermail/linux/kernel/0004.0/0728.html


在文章 <200004042249.SAA06325@op.net> 中,Paul Barton-Davis 写道:

我感到非常沮丧的是,在我的系统上,mmap/mlock 方法所需的时间比 read 解决方案多了三倍。在我看来,mmap/mlock 至少应该与 read 一样快。欢迎评论。

人们喜欢使用 mmap() 和其他方式来处理页面表以优化复制操作,有时确实值得这样做。

然而,玩弄虚拟内存映射本身就非常昂贵。它有许多相当真实的缺点,人们往往忽视这些缺点,因为内存复制被认为是非常缓慢的,有时将其优化掉被视为明显的改进。

mmap 的缺点:

  • 设置和拆除成本相当高。这意味着遵循页表以清理所有映射的工作。它需要维护所有映射列表的簿记。解除映射后需要 TLB 刷新。

  • 页面故障成本很高。这是映射被填充的原因,并且它非常缓慢。

mmap 的优点:

  • 如果数据一次又一次地被重复使用(在单个映射操作中),或者如果你可以通过映射某些东西来避免许多其他逻辑,那么 mmap() 就是自面包片以来的最好东西。这可能是一个你要多次检查的文件(可执行文件的二进制映像就是明显的例子-代码跳到了各个地方),或者一个设置,在这种情况下,将整个东西映射进去非常方便,而不考虑实际使用模式,mmap() 就会获胜。你可能拥有随机访问模式,并使用 mmap() 作为跟踪实际需要的数据的一种方式。

  • 如果数据很大,mmap() 是让系统知道它可以对数据集做什么的好方法。内核可以根据内存压力强制系统分页并忘记页面,然后再次自动获取它们。

自动共享显然是其中之一..

但您的测试套件(仅复制数据一次)可能对 mmap() 来说是最劣的情况。

Linus


最近的结果很奇怪,看起来 mmap() 的读取性能在处理大文件时要么与 read() 相当,要么甚至超过了它。因此,在某些情况下,使用 mmap() 可能会有所帮助。 - sourcejedi
@sourcejedi:链接在哪里?他们使用了哪个块大小?另请参见https://eklitzke.org/efficient-file-copying-on-linux。 - datenwolf
链接1中的64K。链接2中的ripgrep目前似乎使用8K。所以说,这是个好观点,谢谢。你提供的链接有些混淆,因为它用预读来解释图表,但是图表数据是针对完全虚拟(在RAM中)的设备/dev/zero的。 - sourcejedi

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