诊断与修复Arm64原子操作的页面错误性能问题
在运行预缓存预热合成基准测试时,我们注意到Ampere CPU出现了异常的性能影响。深入调查后发现,与x86 CPU相比,Ampere CPU发生了更多的页面错误。
问题描述
问题出现在一个测试缓存预热时间的合成基准测试中,该测试通过执行原子指令向缓冲区每个成员加0。使用性能分析工具对Ampere和x86 CPU的预热阶段进行分析后,我们发现使用透明大页和内存预触时,Ampere上的页面错误数量远高于x86。
在启动阶段后,Ampere的性能仍然受到影响。在/proc/vmstat目录中,我们看到Ampere系统上的thp_split_pmd计数器(指示内存中碎片化大页的数量)远高于预期。这表明在预热过程中,透明大页被碎片化,导致了性能问题。
Arm64原子指令与页面错误
自ARM 8.1-A以来,Ampere和其他Arm64平台可以使用大系统扩展原子指令。在引入LSE之前,Arm架构使用单独的指令来加载寄存器值,并在保存更新值前检查寄存器是否发生变化。
使用LSE后,Arm引入了一套单指令原子操作,包括ldadd指令,该指令在一个指令中执行加载、相加和存储操作。虽然这是单条CPU指令,但由于Arm64上这些原子指令的实现方式,该指令首先会生成对应于加载寄存器值的"读取"错误,然后生成对应于存储新值的单独"写入"错误,导致产生过多的页面错误。
性能影响
这对性能产生负面影响的原因包括:
- 页面错误及随之所需的TLB维护会显著影响常规操作性能
- 使用透明大页时,这会导致大页被拆分。当首次引用大页时,内核会提供一个单独的"大"零页,但随着内存被写入,这会为每个内存页触发内核中的"写时复制",导致分配单独的内存页而不是预期的连续内存块
内存管理概述
操作系统为用户空间程序创建虚拟内存地址空间,并管理虚拟内存地址到物理内存地址的映射。Linux还为用户空间程序提供透明大页功能,允许这些程序保留大的连续物理内存块,并将该内存视为单个页面。
当应用程序访问虚拟内存地址时,操作系统首先检查该虚拟内存地址是否在已存在于内存中的物理内存页中。内核通过检查其页表(从虚拟内存地址到物理内存地址的映射集合)来完成此操作。
解决方案
解决此问题主要有两种方法:
首先,对于内存预热,可以使用替代机制来预触内存。Linux内核提供系统调用madvise(),允许应用程序开发者指示与内存相关的意图,并就应用程序如何使用某些内存段向内核提供建议。
我们发现,在启动带有–XX:+UseTransparentHugePages –CC:+AlwaysPreTouch的JVM时,更新JVM的行为以调用madvise(addr, len, MADV_POPULATE_WRITE)来向内核表明我们打算写入此内存区域,这完全避免了原子指令与内存预热之间的交互。
其次,我们正在与Linux内核社区合作,确保在使用THP时,对大零页的写入错误会在内存中分配一个大页。虽然此问题尚未完全解决,但Linux内核社区正在开发补丁,我们预计问题很快就会得到修复。
测试结果
在我们的测试中,进行这样的更改使得"内存预热"基准测试中的页面错误数量减少了一半,并在运行于Ampere Altra CPU的虚拟机上将该操作的时间减少了60%。
参考资料
- “Transparent Huge Page support”:https://docs.kernel.org/admin-guide/mm/transhuge.html
- “v5 Patch: mm: Force write fault for atomic RMW instructions”:https://lore.kernel.org/lkml/20240626191830.3819324-1-yang@os.amperecomputing.com/
- “Huge Zero page confusion”:https://lore.kernel.org/linux-mm/1cfae0c0-96a2-4308-9c62-f7a640520242@arm.com/
- “pretouch_memory by atomic-add-0 fragments huge pages unexpectedly”:https://bugs.openjdk.org/browse/JDK-8272807