1)现代操作系统(Linux——对我来说最有趣;FreeBSD、OSX、Windows)及其 realloc 实现是否真的能够使用虚拟到物理映射的重新排序而不是逐字节重新分配数据页面复制?
2) 用于实现此内存移动的系统调用是什么?(我认为它可以与 SPLICE_F_MOVE 拼接,但它有缺陷并且现在无法操作(?))
请参阅thejh的答案。
演员是谁?
您的 Qt 示例至少有三个演员。
- Qt 向量类
- glibc的
realloc()
- Linux的
mremap
QVector::capacity()
表明 Qt 分配了比所需更多的元素。这意味着一个元素的典型添加将不会发生realloc()
任何事情。glibc分配器基于Doug Lea 的分配器。这是一个分箱分配器,支持使用 Linux 的mremap
. binning分配器将类似大小的分配分组到bins中,因此典型的随机大小的分配仍然有一些增长空间,而无需调用系统。即,空闲池或slack 位于分配内存的末尾。
答案
3) 使用这种页面改组而不是逐字节复制是否有利可图,尤其是在多核多线程世界中,虚拟到物理映射的每次更改都需要从所有 TLB 中刷新(无效)更改的页表条目带有 IPI 的 CPU 内核数?(在 linux 中,这有点像 flush_tlb_range 或 flush_tlb_page)
首先,比 mremap 更快地被误用mremap()
,正如R所指出的那样。
有几件事情mremap()
使realloc()
.
- 减少内存消耗。
- 保留页面映射。
- 避免移动数据。
此答案中的所有内容均基于 Linux 的实现,但语义可以转移到其他操作系统。
减少内存消耗
考虑幼稚 realloc()
。
void *realloc(void *ptr, size_t size)
{
size_t old_size = get_sz(ptr); /* From bin, address, map table, etc */
if(size <= old_size) {
resize(ptr);
return ptr;
}
void * new_p = malloc(size);
if(new_p) {
memcpy(new_p, ptr, old_size); /* fully committed old_size + new size */
free(ptr);
}
return new_p;
}
为了支持这一点,realloc()
在进行交换之前,您可能需要双倍的内存,或者根本无法重新分配。
保留页面映射
默认情况下,Linux 会将新分配映射到零页;一个充满零数据的 4k 页。这对于稀疏映射的数据结构很有用。如果没有人写入数据页,则除了可能的表之外没有分配物理内存。PTE
这些是写时复制或COW。通过使用naive realloc()
,这些映射将不会被保留,并且会为所有零页分配完整的物理内存。
如果任务涉及 a fork()
,则初始realloc()
数据可能在父子之间共享。同样,COW将导致页面的物理分配。幼稚的实现会忽略这一点,并且每个进程需要单独的物理内存。
如果系统处于内存压力之下,现有realloc()
页面可能不在物理内存中,而是在交换中。天真 将导致交换页面的realloc
磁盘读取到内存中,复制到更新的位置,然后可能将数据写入磁盘。
避免移动数据
与数据相比,您考虑更新TLB的问题是最小的。单个TLB通常为 4 个字节,代表一页 (4K) 物理数据。如果为 4GB 系统刷新整个TLB,则需要恢复 4MB 数据。复制大量数据会破坏 L1 和 L2 缓存。 TLB自然地比d-cache和i-cache更好地获取管道。由于大多数代码是连续的,因此代码很少会连续发生两次TLB未命中。
CPU 有两种变体,VIVT(非 x86)和VIPT(根据x86 ) 。VIVT版本通常具有使单个TLB条目无效的机制。对于VIPT系统,缓存不需要因为它们被物理标记而失效。
在多核系统上,在所有核上运行一个进程是不典型的。只有执行进程的内核才mremap()
需要更新页表。当进程迁移到核心(典型的上下文切换)时,无论如何它都需要迁移页表。
结论
您可以构建一些病态的案例,在这些案例中,幼稚的副本会更好地工作。由于 Linux(和大多数操作系统)用于多任务处理,因此将运行多个进程。此外,最坏的情况是交换时,天真的实现在这里总是会更糟(除非你的磁盘比内存快)。对于最小realloc()
尺寸,dlmalloc或QVector应该有空闲空间以避免系统级别mremap()
。典型的mremap()
可能只是通过使用来自空闲池的随机页面来扩展区域来扩展虚拟地址范围。只有当虚拟地址范围必须移动mremap()
可能需要tlb flush,以下所有情况均属实,
realloc()
内存不应与父进程或子进程共享。
- 内存不应稀疏(大部分为零或未触及)。
- 系统不应使用交换处于内存压力之下。
tlb 刷新和 IPI 仅在其他内核上存在相同进程时才需要进行。不需要加载 L1 缓存mremap()
,但对于naive版本是必需的。L2 通常在内核之间共享,并且在所有情况下都是最新的。天真的版本将强制 L2 重新加载。mremap()
可能会将一些未使用的数据留在二级缓存之外;这通常是一件好事,但在某些工作量下可能是一个缺点。可能有更好的方法来做到这一点,例如预取数据。