是真的吗,现代操作系统在调用 realloc 时可能会跳过复制

Posted

技术标签:

【中文标题】是真的吗,现代操作系统在调用 realloc 时可能会跳过复制【英文标题】:Is it true, that modern OS may skip copy when realloc is called 【发布时间】:2013-05-21 20:24:52 【问题描述】:

在阅读https://***.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) 使用这种页面改组而不是逐字节复制是否有利可图,尤其是在多核多线程世界中,虚拟到物理映射的每次更改都需要从 @ 刷新(无效)更改的页表条目987654324@s 在所有数十个 CPU 内核中与IPI? (在 linux 中,这有点像 flush_tlb_range or flush_tlb_page

第三季度更新:some tests of mremap vs memcpy

【问题讨论】:

realloc()C 库中实现。在 Linux 上,libc 通常是 Doug Lea's malloc 的 eglibc/glibc 版本。这是一个分箱分配器,它是特殊的 HAVE_MREMAP,默认情况下是为 linux 定义的。 splice() 是一个完全不同的概念。 TLB 无效通常是 4 字节。因此,除非 realloc1024*4k/10 个内核 或 ~512KB,否则 mremap() 会更好。它仍然可能更好,因为副本也会炸毁 d-cache "TLB 无效通常是 4 字节。" - 是错字吗? TLB 无效是 IPI 并写入 CR3 以重置所有 TLB 行。 无艺术的噪音,PTE条目的大小很小;但是我们不仅要更新内存中的页表,还要更新TLB条目。通常无法直接访问各个 TLB 行,因此仍需要完整的 TLB 刷新。如果我要求 realloc,我就触动了内存。 不在ARM上,你可以invalidate individual TLB's。但如前所述,这是最坏的情况。 mremap() 可能只是扩展虚拟范围以映射额外的物理页面(来自空闲池中的随机地址)。如果realloc() 内存是稀疏,那么一半以上的页面甚至可能没有被触及,并且许多虚拟页面可能映射到零页面。复制会增加用于此 sparse 用例的内存。 【参考方案1】:

1) 现代操作系统(Linux - 对我来说最有趣;FreeBSD、OSX、Windows)和它们的 realloc 实现真的能够使用虚拟到物理映射的重新排序并且无需字节来重新分配数据页面吗?按字节复制?

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

查看thejh的回答。

演员是谁?

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

    Qt 向量类 glibcrealloc() Linux 的mremap

QVector::capacity() 表明 Qt 分配的元素比需要的多。这意味着一个元素的典型添加不会realloc() 任何东西。 glibc 分配器基于Doug Lea's allocator。这是一个 binning 分配器,它支持使用 Linux 的 mremapbinning 分配器将类似大小的分配分组在 bins 中,因此典型的随机大小分配仍然有一些增长空间,而无需调用系统。即,空闲池或 slack 位于分配内存的末尾。

答案

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 缓存。 TLBd-cachei-cache 更自然地获取管道。由于大多数代码是连续的,因此代码很少会连续发生两次 TLB 未命中。

CPU 为 two variants、VIVT非 x86)和 VIPT,根据 x86VIVT 版本通常具有使单个 TLB 条目无效的机制。对于VIPT 系统,缓存 不需要因为它们被物理标记而失效。

在多核系统上,在所有核上运行一个进程是不典型的。只有进程执行mremap() 的内核需要更新页表。当一个进程迁移到一个核心(典型的上下文切换)时,它无论如何都需要迁移页表。

结论

您可以构建一些病态的情况,在这些情况下,天真的副本会更好地工作。由于 Linux(和大多数操作系统)用于多任务,因此将运行多个进程。此外,最坏的情况是交换时,naive 实现在这里总是会更糟(除非你的磁盘比内存快)。对于最小的realloc() 大小,dlmallocQVector 应该有 fallow 空间以避免系统级别的mremap()。典型的mremap() 可能只是通过使用来自空闲池 的随机页面扩展区域来扩展虚拟地址范围。只有当 虚拟地址范围 必须移动时,mremap() 才可能需要 tlb 刷新,以下所有情况均成立,

    realloc() 内存不应与父进程或子进程共享。 内存不应稀疏(大部分为零或未触及)。 系统不应使用交换处于内存压力之下。

tlb 刷新 和 IPI 仅在同一进程在其他内核上处于当前状态时才需要发生。 mremap() 不需要加载 L1 缓存,但对于 naive 版本是必需的。 L2 通常在内核之间共享,并且在所有情况下都是最新的。 naive 版本将强制 L2 重新加载。 mremap() 可能会在二级缓存之外留下一些未使用的数据;这通常是一件好事,但在某些工作量下可能是一个缺点。可能有更好的方法来做到这一点,例如预取数据。

【讨论】:

仍然无法理解每个 TLB 的 4 字节。这是从启动 mremap 的内核到所有其他内核的内核间中断;我们要求他们部分冲洗他们的 tlb。 TLB 不是 PTE,TLB 是 PTE 的 CACHE。如果我们更改内存中的 PTE 但不刷新 TLB 的 PTE 副本,此缓存将失去一致性。 您对 q1 的回答确实是“正确”——有些系统可以这样做,谢谢! 单个 TLB 条目映射一个 Linux 页面。 Linux 页面是 4k 的内存。每个 TLB就像一个指针,平均只有 4 个字节。因此,即使您在一个完整的 4GB 系统上刷新整个 TLB 缓存,TLB 条目也只有 4MB ((4GB/4k)*4)。每个 TLB 条目是一个PTE;对于一个完整的 4GB 系统,PTEs 有 4MB 的价值。单个TLB 条目是PTE; TLB 是一个 MMU 缓存。 而实际上,整个TLB cache通常被限制在1k到16k的大小;它通常不需要像 i-cached-cache 那样大,因为单个条目覆盖了更多的地址空间。重新加载它会影响性能,但不像其他缓存那样昂贵;对于mremap(),这是最坏的情况。 jemalloc 也曾使用过mremap,但在 4.0 (2015) 版本中已被删除;所以显然 modern 发生了变化。它被列为与多线程应用程序的一致性存在问题。但是,我没有找到具体细节。【参考方案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 参数和可用的虚拟地址空间控制)。

【讨论】:

realloc 使用它吗?另外,有人说OSX中没有mremap:***.com/questions/3521303/… 并且根据ptgmedia.pearsoncmg.com/images/0131453483/samplechapter/…第3章第44页表3.2,mremap确实调用flush_tlb_range

以上是关于是真的吗,现代操作系统在调用 realloc 时可能会跳过复制的主要内容,如果未能解决你的问题,请参考以下文章

realloc() 在嵌入式系统中安全吗?

我可以假设调用较小大小的 realloc 会释放其余部分吗? [复制]

realloc() 失败并返回 NULL 时的正确用法是啥?

你真的了解JMM吗?

realloc 会覆盖旧内容吗?

将 realloc() 返回的地址分配给同一个指针是一种好的编码习惯吗?