解决Arm64原子操作导致的页错误性能问题
在运行预缓存预热合成基准测试时,我们注意到Ampere CPU出现了异常的性能影响。深入调查后,我们发现与x86 CPU相比,Ampere CPU发生了更多的页错误。我们将问题隔离到某些原子指令(如ldadd)的使用上,这些指令在单个指令中加载寄存器、增加值并将数据存储到寄存器中。在某些条件下,这会触发两次"页错误",尽管这在逻辑上是一个全有或全无的操作,保证在一个步骤中完成。
问题描述
该问题由一个合成基准测试发现,该测试通过向缓冲区每个成员执行原子加0指令来测试缓存"预热"时间。使用性能分析工具,我们能够在Ampere和x86 CPU上分析基准测试的预热阶段,并查看额外时间花费在哪里。我们使用perf发现,在使用透明大页和内存预触时,Ampere上的页错误数量比x86多得多。
在启动阶段后,Ampere的性能仍然受到影响,在/proc/vmstat目录中,我们观察到Ampere系统上的thp_split_pmd计数器(指示内存中碎片化大页的数量)比预期高得多。这表明在预热过程中,透明大页被碎片化,导致性能问题。
Arm64原子指令与页错误
在ARM 8.1-A及以后的Arm64平台上,原子指令可以使用大系统扩展(LSE)系列原子指令。使用LSE,Arm引入了单指令原子操作,包括ldadd,它执行加载、添加和存储操作。您可能期望单个CPU指令只生成一个页错误。然而,由于这些原子指令在Arm64上的实现方式,这个单指令首先生成一个"读"错误(对应加载寄存器值),然后生成一个单独的"写"错误(对应存储新值)。因此,这导致了过多的页错误。
这在几个方面是不利的:
- 在常规操作中,这会显著影响性能,因为页错误和随之所需的TLB维护会显著影响性能
- 在使用透明大页时,这会导致大页被拆分
内存管理概述
操作系统为用户空间运行的程序创建虚拟内存地址空间,并管理虚拟内存地址到物理内存地址的映射。此外,Linux为用户空间程序提供了一个称为透明大页的功能,允许这些程序保留大的连续物理内存块,并将该内存视为单个页面,尽管实际上这些大页由许多连续的物理内存页面组成。
当应用程序访问虚拟内存地址时,操作系统首先检查该虚拟内存地址是否已在内存中的物理内存页中。内核通过检查其页表(从虚拟内存地址到物理内存地址的映射集合)来完成此过程。此过程的第一步是检查转换后备缓冲区,它存储相对较少的页表条目以加速对最常用内存位置的访问。
解决方案
解决这个问题实际上归结为两件事:
首先,对于内存预热,可以使用替代机制来预触内存。Linux内核提供了一个系统调用madvise(),允许应用程序开发人员指示与内存相关的意图,并就应用程序将如何使用某些内存部分向内核提供建议。这使内核能够主动使用适当的缓存或预读技术来提高性能。
其次,我们正在与Linux内核社区合作,确保在使用THP时,对大零页的写错误会在内存中分配一个大页。虽然这个问题尚未完全解决,但Linux内核社区正在开发补丁,我们预计问题很快就会得到解决。
对于常规页面,由于这些指令在ARM中的实现方式,Linux内核将继续通过原子加法指令触发两个页错误。我们认为原子"读-修改-写"指令应该只生成一个写错误,这将提高这些操作的性能。在我们的测试中,进行这样的改变使"内存预热"基准测试中的页错误数量减少了一半,并在运行在Ampere Altra CPU上的虚拟机上将操作时间减少了60%。