虚拟内存映射碎片会导致性能问题吗?
Does virtual memory mapping fragmentation cause performance issue?
我有一个应用程序,我首先使用 linux mmap
系统调用创建大型匿名映射(大约 4MB)。
然后在进程执行的过程中,为了尽快释放内存,我在考虑取消映射更小的内存块,这样一开始是大块的虚拟内存映射就会变得碎片化。
这可能会由于虚拟内存转换 table 碎片而导致性能问题,还是内核使用智能策略来避免这种情况?我可以不介意虚拟内存映射的碎片吗?
简短回答: 除非您随机分配大量最小粒度(即 4096 字节)的非连续区域。
长答案:有点,
在现代架构上,您有多个级别的虚拟内存映射(或页面 tables,无论您喜欢哪个术语),对于 64 位架构,48 位地址通常有 4 级内存映射space(英特尔即将推出的扩展将添加另一个级别,允许将 4096 字节的页面分段为 256 字节的页面)。每次在不存在映射的区域中分配页面时,内核必须分配一个新的(通常 物理上 连续;注意重点,相对而言,这是一个相当昂贵的操作) 内存块,用于保存该内存区域的翻译映射 space。我将避免使用特定于架构的术语,而将它们称为 L0 -> L1 -> L2 -> L3
,其中 L0
是代表该虚拟内存 space 的根映射。这将随页面大小粒度和不同的操作系统或体系结构而变化(例如 Linux 有超级页面)。
现在,如果您的新映射处于 L3 级别,并且有一个 L3 页面 table,则新映射将只涉及更改该区域中的条目以指示翻译。如果没有L3页面table,则必须分配一个新的L3页面table,并进入L2页面table。依此类推到L0页table.
几个快速笔记:
- 每次更改映射时,通常都会有一个 TLB(转换后备缓冲区;MMU 用于 VM->Phys 转换的硬件缓存)失效惩罚(无论是手动还是自动)。
- 有些页面可能不需要所有 4 个翻译阶段,翻译级别有特定的大小,因此超级页面通常是一个页面,例如使用 L2 页面 table 条目映射整个块VM space 到物理 space(这意味着只需要 3 个级别的转换)。
- 各种架构使用不同的方法来减少 TLB 垃圾处理的惩罚(即 x86_64 上的 PCID;事实上,没有它,像 KPTI 这样的崩溃缓解措施会导致性能下降)。
- 说到崩溃,某些内存范围可能具有内核或蹦床映射或异常向量。这些由 OS 保留。在 64 位系统 pre-Spectre/Meltdown 上,内核将自身映射到每个页面 table 是很常见的。很多 ARM 处理器都有一个专门的机制,叫做 split page tables (TTBR0/TTBR1; Translation Table Base Register 0/1).
- 上面的一个例子是Linux VDSO(虚拟动态共享对象),它是由内核创建的映射。达尔文 (OSX/iOS) 与之对应的是 commpage(公共页面)。这通常具有系统中每个进程共享的只读代码,并且具有当前时间(为了降低系统调用的成本,
gettimeofday
可以从 VDSO 读取它或使用 VDSO 蹦床读取它)。
- 当然,以上所有内容都会根据体系结构和您使用的 OS 以及您使用的 OS 的版本而有所不同,因为虚拟内存管理器经常使用各种确保不发生碎片化的技术。 但是,如果您随机请求很多小的固定映射,是的,您实际上会绕过很多映射,从而导致性能问题。
我有一个应用程序,我首先使用 linux mmap
系统调用创建大型匿名映射(大约 4MB)。
然后在进程执行的过程中,为了尽快释放内存,我在考虑取消映射更小的内存块,这样一开始是大块的虚拟内存映射就会变得碎片化。
这可能会由于虚拟内存转换 table 碎片而导致性能问题,还是内核使用智能策略来避免这种情况?我可以不介意虚拟内存映射的碎片吗?
简短回答: 除非您随机分配大量最小粒度(即 4096 字节)的非连续区域。
长答案:有点,
在现代架构上,您有多个级别的虚拟内存映射(或页面 tables,无论您喜欢哪个术语),对于 64 位架构,48 位地址通常有 4 级内存映射space(英特尔即将推出的扩展将添加另一个级别,允许将 4096 字节的页面分段为 256 字节的页面)。每次在不存在映射的区域中分配页面时,内核必须分配一个新的(通常 物理上 连续;注意重点,相对而言,这是一个相当昂贵的操作) 内存块,用于保存该内存区域的翻译映射 space。我将避免使用特定于架构的术语,而将它们称为 L0 -> L1 -> L2 -> L3
,其中 L0
是代表该虚拟内存 space 的根映射。这将随页面大小粒度和不同的操作系统或体系结构而变化(例如 Linux 有超级页面)。
现在,如果您的新映射处于 L3 级别,并且有一个 L3 页面 table,则新映射将只涉及更改该区域中的条目以指示翻译。如果没有L3页面table,则必须分配一个新的L3页面table,并进入L2页面table。依此类推到L0页table.
几个快速笔记:
- 每次更改映射时,通常都会有一个 TLB(转换后备缓冲区;MMU 用于 VM->Phys 转换的硬件缓存)失效惩罚(无论是手动还是自动)。
- 有些页面可能不需要所有 4 个翻译阶段,翻译级别有特定的大小,因此超级页面通常是一个页面,例如使用 L2 页面 table 条目映射整个块VM space 到物理 space(这意味着只需要 3 个级别的转换)。
- 各种架构使用不同的方法来减少 TLB 垃圾处理的惩罚(即 x86_64 上的 PCID;事实上,没有它,像 KPTI 这样的崩溃缓解措施会导致性能下降)。
- 说到崩溃,某些内存范围可能具有内核或蹦床映射或异常向量。这些由 OS 保留。在 64 位系统 pre-Spectre/Meltdown 上,内核将自身映射到每个页面 table 是很常见的。很多 ARM 处理器都有一个专门的机制,叫做 split page tables (TTBR0/TTBR1; Translation Table Base Register 0/1).
- 上面的一个例子是Linux VDSO(虚拟动态共享对象),它是由内核创建的映射。达尔文 (OSX/iOS) 与之对应的是 commpage(公共页面)。这通常具有系统中每个进程共享的只读代码,并且具有当前时间(为了降低系统调用的成本,
gettimeofday
可以从 VDSO 读取它或使用 VDSO 蹦床读取它)。 - 当然,以上所有内容都会根据体系结构和您使用的 OS 以及您使用的 OS 的版本而有所不同,因为虚拟内存管理器经常使用各种确保不发生碎片化的技术。 但是,如果您随机请求很多小的固定映射,是的,您实际上会绕过很多映射,从而导致性能问题。