考虑一个使用大量页面大小的内存区域(比如64 kB左右)的程序,每个区域都是相当短暂的.(在我的特定情况下,这些是绿色线程的备用堆栈.)
如何最好地分配这些区域,以便一旦该区域不再使用,它们的页面可以返回到内核?天真的解决方案显然只是mmap
单独的每个区域,munmap
一旦我完成它们就会再次出现.不过,我觉得这是一个坏主意,因为它们有很多.我怀疑VMM可能会在一段时间后开始严重缩放; 但即使没有,我仍然对理论案例感兴趣.
如果我只是mmap
自己一个巨大的匿名映射,我可以根据需要分配区域,有没有办法通过映射"打孔"我已完成的区域?有点像madvise(MADV_DONTNEED)
,但不同之处在于应该将页面视为已删除,以便内核实际上不需要将其内容保留在任何位置,但只要再次出现故障就可以重用已归零的页面.
我正在使用Linux,在这种情况下,我并不打算使用特定于Linux的调用.
在某些方面,我对这个主题做了很多研究(针对不同的用途).在我的情况下,我需要一个非常稀疏填充的大型hashmap +偶尔将其归零的能力.
mmap
方案:
最简单的解决方案(可移植,madvise(MADV_DONTNEED)
特定于Linux)将这样的映射归零到mmap
它上面的新映射.
void * mapping = mmap(MAP_ANONYMOUS);
// use the mapping
// zero certain pages
mmap(mapping + page_aligned_offset, length, MAP_FIXED | MAP_ANONYMOUS);
Run Code Online (Sandbox Code Playgroud)
最后一次调用是性能明智的,等同于后续munmap/mmap/MAP_FIXED
,但是线程安全.
性能方面,该解决方案的问题在于,必须在子序列写访问中再次出现故障,该子访问会发出中断和上下文更改.这只有在首先出现故障很少的页面时才有效.
memset
解:
如果大部分映射必须取消映射,那么在具有这样的废话性能后,我决定手动将内存归零memset
.如果大约70%以上的页面已经出现故障(如果不是,那么它们是在第一轮之后memset
),那么这比重新映射这些页面更快.
mincore
解:
我的下一个想法实际上只是memset
在那些之前出现故障的页面上.此解决方案不是线程安全的.调用mincore
以确定页面是否出现故障然后选择性地将memset
它们归零是一个显着的性能改进,直到超过50%的映射出现故障,此时memset
整个映射变得更简单(mincore
是系统调用并需要一个上下文更改) ).
内容表解决方案:
我接下来的最后一个方法就是拥有自己的核心表(每页一位),说明自上次擦除后是否已经使用过.这是迄今为止最有效的方法,因为您实际上只会将实际使用的每一轮中的页面归零.它显然也不是线程安全的,并且要求您跟踪在用户空间中写入的页面,但是如果您需要此性能,那么这是迄今为止最有效的方法.
mmap
我不明白为什么对/进行大量调用munmap
会那么糟糕。内核中映射的查找性能应为 O(log n)。
现在似乎在 Linux 中实现的唯一选择是在映射中打孔来执行您想要的操作,但这mprotect(PROT_NONE)
仍然会碎片化内核中的映射,因此它基本上相当于mmap
/ munmap
,除了其他东西不会能够从您那里窃取 VM 范围。你可能想要madvise(MADV_REMOVE)
一份工作,或者正如 BSD 中所说的那样 - madvise(MADV_FREE)
。这是明确设计的,旨在完全满足您的需求 - 回收页面而不碎片化映射的最便宜的方法。但至少根据我的两种 Linux 风格的手册页,它并没有完全实现所有类型的映射。
免责声明:我最熟悉 BSD VM 系统的内部结构,但这在 Linux 上应该非常相似。
正如下面评论中的讨论一样,令人惊讶的是,MADV_DONTNEED
似乎可以做到这一点:
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#include <err.h>
int
main(int argc, char **argv)
{
int ps = getpagesize();
struct rusage ru = {0};
char *map;
int n = 15;
int i;
if ((map = mmap(NULL, ps * n, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)) == MAP_FAILED)
err(1, "mmap");
for (i = 0; i < n; i++) {
map[ps * i] = i + 10;
}
printf("unnecessary printf to fault stuff in: %d %ld\n", map[0], ru.ru_minflt);
/* Unnecessary call to madvise to fault in that part of libc. */
if (madvise(&map[ps], ps, MADV_NORMAL) == -1)
err(1, "madvise");
if (getrusage(RUSAGE_SELF, &ru) == -1)
err(1, "getrusage");
printf("after MADV_NORMAL, before touching pages: %d %ld\n", map[0], ru.ru_minflt);
for (i = 0; i < n; i++) {
map[ps * i] = i + 10;
}
if (getrusage(RUSAGE_SELF, &ru) == -1)
err(1, "getrusage");
printf("after MADV_NORMAL, after touching pages: %d %ld\n", map[0], ru.ru_minflt);
if (madvise(map, ps * n, MADV_DONTNEED) == -1)
err(1, "madvise");
if (getrusage(RUSAGE_SELF, &ru) == -1)
err(1, "getrusage");
printf("after MADV_DONTNEED, before touching pages: %d %ld\n", map[0], ru.ru_minflt);
for (i = 0; i < n; i++) {
map[ps * i] = i + 10;
}
if (getrusage(RUSAGE_SELF, &ru) == -1)
err(1, "getrusage");
printf("after MADV_DONTNEED, after touching pages: %d %ld\n", map[0], ru.ru_minflt);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我正在ru_minflt
作为代理进行测量,以了解我们需要分配多少页面(这并不完全正确,但下一句话使其更有可能)。我们可以看到在第三个 printf 中我们得到了新的页面,因为 的内容map[0]
是 0。