现代操作系统在调用realloc时是否真的会跳过复制过程?

12

阅读https://dev59.com/LHA75IYBdhLWcg3wo6pe#3190489时,我有一个问题。Qt 4容器内部的Inside the Qt 4 Containers中作者所说的是什么:

... QVector使用realloc()以4096字节的增量增长。这是有道理的,因为现代操作系统在重新分配缓冲区时不会复制整个数据;物理内存页面只是重新排序,只有第一页和最后一页上的数据需要被复制。

我的问题是:

1)现代操作系统(Linux - 对我来说最有趣的; FreeBSD、OSX、Windows)及其realloc实现是否真的能够使用虚拟到物理映射的重新排序而无需逐字节复制来重新分配数据页?

2)用于实现此内存移动的系统调用是什么?(我认为它可以是splice与SPLICE_F_MOVE一起使用,但它有缺陷并且现在是无操作的(?))

3)在多核多线程的世界中,使用这种页面重排是否比逐字节复制更有利可图,其中每次虚拟到物理映射的更改都需要从所有十个CPU核心的TLB中刷新(使无效)已更改的页表项,使用IPI?(在Linux中,这类似于flush_tlb_rangeflush_tlb_page

更新第3个问题:一些mremap与memcpy的测试


1
realloc()是在C库中实现的。在Linux上,libc通常是Doug Lea's malloceglibc/glibc版本。这是一个分配器,它具有特殊的HAVE_MREMAP,默认情况下为linux定义。 splice()是一个完全不同的概念。 TLB无效通常为4字节。因此,除非realloc1024*4k/10 cores或约512KB,否则mremap()更好。它仍然可能比复制更好,因为复制将会使d-cache爆炸。 - artless noise
"TLB失效通常为4个字节。" - 这是一个打字错误吗?TLB失效是IPI,并写入CR3以重置所有TLB行。 - osgx
毫无意义的噪音,PTE条目的大小很小;但我们不仅应该更新内存中的页表,还必须更新TLB条目。通常没有直接访问单个TLB行的方法,因此仍然需要完全刷新TLB。如果我请求重新分配内存,则会触及内存。 - osgx
1
不是在ARM上,您可以使单个TLB无效。 但是要注意,这是最坏的情况。 mremap()可能只会扩展虚拟范围以映射另一页物理页(从空闲池中的随机地址)。 如果realloc()内存是稀疏的,则超过一半的页面甚至可能没有被触及,并且许多虚拟页面可能映射到零页面。 复制将增加用于此稀疏用例的内存使用。 - artless noise
2个回答

10
  1. 现代操作系统(Linux - 最让我感兴趣的; FreeBSD、OSX、Windows)和它们的realloc实现是否真正能够使用虚拟到物理映射的重排而无需逐字节复制来重新分配数据页?
  1. 用于实现此内存移动的系统调用是什么?(我认为它可以使用SPLICE_F_MOVE和splice,但它已经有缺陷并且现在是无操作的(?))

请参见thejh的答案。

谁是演员?

在您的Qt示例中,至少有三个演员。

  1. Qt向量类
  2. glibcrealloc()
  3. Linux的mremap

QVector::capacity()显示Qt分配了比所需更多的元素。这意味着添加一个元素通常不会realloc()任何东西。 glibc分配器基于Doug Lea的分配器。这是一个binning分配器,支持使用Linux的mremapbinning分配器将类似大小的分配分组到bins中,因此典型的随机大小分配仍将具有一些增长空间,而无需调用系统。 即,自由池或松弛位于已分配内存的末尾。

一个答案

  1. 在多核多线程的世界中,使用这种页面重排而不是逐字节复制是否有益处,每次更改虚拟到物理映射都需要在所有数十个CPU核心中刷新(使无效)更改的页表项,其中包括IPI? (在Linux中,这就像flush_tlb_range或flush_tlb_page)

首先,比mremap还快的方法误用了mremap(),如R在那里所指出的。

有几个因素使mremap()作为realloc()的基本要素具有价值。

  1. 降低内存消耗。
  2. 保留页面映射。
  3. 避免移动数据。

本答案中的所有内容都基于Linux的实现,但语义可以转移到其他操作系统。

减少内存消耗

考虑一个naiverealloc()

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表之外,不会分配任何物理内存。这些是写时复制Copy-On-WriteCOW。但是,使用naive(幼稚的)realloc()将不会保留这些映射,并为所有零页分配完整的物理内存。

如果任务涉及fork(),则初始realloc()数据可能在父进程和子进程之间共享。同样,COW会导致页面的物理分配。naive(幼稚的)实现会忽略这一点,并要求每个进程单独分配物理内存。

如果系统承受着内存压力,则现有的realloc()页面可能不在物理内存中,而在交换空间中。naive(幼稚的)realloc()将导致将交换页面从磁盘读入内存,复制到更新位置,然后可能将数据写回磁盘。

避免数据移动

您考虑的更新TLBs的问题与数据相比很小。单个TLB通常为4字节,并表示4K的物理数据页。如果刷新整个TLB,对于4GB系统来说,需要恢复4MB的数据。复制大量数据将使L1和L2缓存失效。TLB获取自然比d-cachei-cache更好地管道化。代码连续的情况下,很少会出现两个TLB未命中的情况。

CPU有两种变体VIVT(非x86)和根据x86VIPTVIVT版本通常具有使单个TLB项无效的机制。对于VIPT系统,不应该需要使缓存无效,因为它们具有物理标记。

在多核系统上,一般不会在所有核心上运行一个进程。只有执行mremap()的进程需要更新页表。当进程迁移到另一个核心时(典型的上下文切换),它需要迁移页面表。

结论

您可以构造一些病态案例,其中naive复制效果更好。由于Linux(以及大多数操作系统)是用于多任务的,因此将运行多个进程。而且,最坏的情况是在交换和naive实现始终更差(除非您拥有比内存更快的磁盘)。对于最小的realloc()大小,dlmallocQVector应该具有空闲空间,以避免系统级mremap()。通常的mremap()只需通过从free pool中使用随机页来扩展虚拟地址范围即可。只有当必须移动虚拟地址范围时,mremap()可能需要tlb flush,并且以下所有内容都为真:
  1. realloc()内存不应与父进程或子进程共享。
  2. 内存不应为sparse(大多数为零或未触及)。
  3. 系统不应在使用交换时受到内存压力。
只有当同一进程在其他核心上时,才需要进行tlb flush和IPI。不需要为mremap()进行L1缓存加载,但对于naive版本需要。 L2通常在核心之间共享,并且在所有情况下都是当前的。 naive版本将强制重新加载L2。 mremap()可能会将一些未使用的数据留在L2缓存外;这通常是一件好事,但在某些工作负载下可能是一个缺点。可能有更好的方法来执行此操作,例如预取数据。

仍然不理解你所说的每个TLB 4字节。这是来自启动mremap的核心的内部中断,传递给所有其他核心; 我们要求它们部分刷新其TLB。TLB不是PTE,TLB是PTE的CACHE。如果我们在内存中更改PTE但不刷新TLB的PTE副本,则此缓存将失去一致性。 - osgx
1
而且你对Q1的答案是“true” - 有一些系统能够做到这一点,谢谢! - osgx
一个TLB条目映射一个Linux页面。一个Linux页面是4k内存。每个TLB就像一个指针,平均只有4字节大小。因此,即使在完整的4GB系统上刷新整个TLB缓存,TLB条目也仅占用4MB((4GB / 4k) * 4)。每个TLB条目都是一个PTE;对于完整的4GB系统,有4MB的PTE。一个单独的TLB条目是一个PTE;TLB是一个MMU高速缓存。 - artless noise
1
实际上,整个TLB缓存通常限制在1k到16k的大小范围内;它通常不需要像i-cache和d-cache那样大,因为单个条目可以覆盖更多的地址空间。重新加载它会影响性能,但不像其他缓存那样昂贵;这是mremap()的最坏情况。 - artless noise
jemalloc 也使用了 mremap,但在 2015 年的4.0版本中已被移除,显然是现代的变化。它被列为与多线程应用程序的一致性存在问题。然而,我没有找到具体信息。 - artless noise
显示剩余2条评论

2

对于Linux,请参见man 2 mremap

   void *mremap(void *old_address, size_t old_size,
                size_t new_size, int flags, ... /* void *new_address */);
函数 可以扩展(或收缩)现有的内存映射,可能同时移动它(由标志参数和可用虚拟地址空间控制)。

它是否被realloc使用?此外,有些人说OSX中没有mremap:https://dev59.com/questions/UHA75IYBdhLWcg3wAUF9 - osgx
根据http://ptgmedia.pearsoncmg.com/images/0131453483/samplechapter/0131453483_ch03.pdf第3章第44页的表格3.2,mremap确实调用了`flush_tlb_range`。 - osgx

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