在阅读https://stackoverflow.com/a/3190489/196561时,我有一个问题。 Qt作者在Inside the Qt 4 Containers中说了什么:
... QVector使用realloc()以4096字节的增量增长。这是有道理的,因为现代操作系统在重新分配缓冲区时不会复制整个数据;物理内存页面只需重新排序,只需要复制第一页和最后一页上的数据。
我的问题是:
1)现代操作系统( Linux - 对我来说最有趣; FreeBSD,OSX,Windows)及其realloc实现是否真的能够使用virtual-to重新排序来重新分配数据页面 - 物理映射,没有逐字节复制?
2)用于实现此内存移动的系统调用是什么? (我认为它可以是splice
SPLICE_F_MOVE,但现在有缺陷且没有操作(?))
3)使用这种页面改组而不是逐字节拷贝是否有利可图,特别是在多核多线程世界中,虚拟到物理映射的每次更改都需要从{更改(无效)更改的页表条目在TLB的所有数十个CPU核心中{3}}? (在linux中,这就像IPI)
答案 0 :(得分:7)
1)现代操作系统(Linux - 对我来说最有趣; FreeBSD,OSX,Windows)及其realloc实现是否真的能够使用虚拟到物理映射的重新排序重新分配数据页面并且没有字节 - 字节副本?
2)用于实现此内存移动的系统调用是什么? (我认为它可以与SPLICE_F_MOVE拼接,但它现在有缺陷和无操作(?))
请参阅thejh的答案。
你的Qt示例中至少有三个演员。
realloc()
mremap
QVector::capacity()
表明Qt分配的元素多于要求的元素。这意味着元素的典型添加不会realloc()
任何东西。 glibc 分配器基于Doug Lea's allocator。这是一个 binning 分配器,支持使用Linux mremap
。 binning 分配器在 bins 中对类似大小的分配进行分组,因此典型的随机大小分配仍然有一定的增长空间,而无需调用系统。即,空闲池或松弛位于已分配内存的末尾。
3)使用这种页面改组而不是逐字节拷贝是否有利可图,特别是在多核多线程世界中,虚拟到物理映射的每次更改都需要从TLB刷新(无效)更改的页表条目在所有数十个带IPI的CPU内核中? (在linux中,这就像flush_tlb_range或flush_tlb_page一样)
首先,faster ... than 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()
,将不会保留这些映射,并为所有零页分配完整的物理内存。
如果任务涉及fork()
,则初始realloc()
数据可能在父母和子女之间共享。同样, COW 将导致页面的物理分配。 naive 实现将对此进行折扣,并且每个进程需要单独的物理内存。
如果系统处于内存压力下,现有的realloc()
页面可能不在物理内存中,而是在交换中。 naive realloc
将导致磁盘读取交换页面到内存,复制到更新的位置,然后可能将数据写入磁盘。
与数据相比,您考虑更新 TLB 的问题很少。单个 TLB 通常为4个字节,表示物理数据的页面(4K)。如果为4GB系统刷新整个 TLB ,则需要恢复4MB数据。复制大量数据会破坏L1和L2缓存。 TLB 自然获取比 d-cache 和 i-cache 更好的管道。由于大多数代码是连续的,代码很少会连续两次出现 TLB 错过。
根据 x86 ,CPU为two variants, VIVT (非x86 )和 VIPT >。 VIVT 版本通常具有使单个 TLB 条目无效的机制。对于 VIPT 系统,缓存不需要在物理标记时失效。
在多核系统上,在所有核心上运行一个进程是不典型的。只有执行mremap()
的进程的核心才需要更新页表。当一个进程迁移到核心(典型的上下文切换)时,它需要迁移页面表。
您可以构建一些病态案例,其中天真副本将更好地工作。由于Linux(以及大多数操作系统)用于多任务,因此将运行多个进程。此外,最糟糕的情况是交换时,天真的实现总是会更糟(除非你的磁盘比内存快)。对于最小realloc()
大小, dlmalloc 或 QVector 应具有 fallow 空间以避免系统级mremap()
。典型的mremap()
可以通过使用免费池中的随机页面增长区域来扩展虚拟地址范围。只有当虚拟地址范围必须移动mremap()
可能需要 tlb flush 时,以下所有内容都为真,
realloc()
内存。 tlb flush 和IPI只有在同一进程在其他核心上是最新的时才需要进行。 mremap()
不需要L1缓存加载,但 naive 版本。 L2通常在核心之间共享,并且在所有情况下都是最新的。 naive 版本将强制L2重新加载。 mremap()
可能会从L2缓存中留下一些未使用的数据;这通常是一件好事,但在一些工作负荷下可能是一个缺点。可能有更好的方法来执行此操作,例如预取数据。
答案 1 :(得分:2)
对于Linux,请参阅man 2 mremap
:
void *mremap(void *old_address, size_t old_size,
size_t new_size, int flags, ... /* void *new_address */);
mremap()扩展(或缩小)现有的内存映射,可能会同时移动它 时间(由flags参数和可用的虚拟地址空间控制)。