Appearance
ASLR开启导致openfoam的性能波动问题
问题表现
本案例中的openfoam是一个benchmark中的一个测试用例,是单线程的任务,测试时会启动N个进程来实现对CPU的压测。
测试过程中发现,ASLR开启时,任务间完成时间差异较大,用时最大可相差一倍,导致整体测试分数不理想。
分析问题前,收集相关信息,并筛选有价值的线索,如下:
- 一个物理核上的两个超线程的任务性能一致;
- 物理核与物理核之间性能无联系;
由上面两条线索,可以限制问题范围在一个物理核上,即这是一个核内问题,但是由于该问题不能稳定浮现,所以进行特征采集时仍需要采集多核。
微架构与热点
可以通过微架构宏观定位一下特征信息。对于本案例,关注正常的性能特征核异常性能的特征区别。
对比之下,异常进程的前端瓶颈很高,接近50%,主要由icache miss贡献。
根据这条线索,可以采集icache miss event的热点,定位到是libopenfoam.so库。重复采集几次后结果一致。
这里乍一看还是挺奇怪的,动态链接库怎么会突然有这么高的icache miss?这里需要和ASLR建立联系。ASLR开启后,内存地址空间会被随机化,导致动态链接库的地址会变化,但是地址变化为什么会导致icache miss增加?
动态库内存映射
利用/proc/pid/maps文件,可以查看进程的内存映射关系,查看libopenfoam.so库的地址。
bash
cat /proc/<pid>/maps输出的内容有内存地址的起止范围,如下:
bash
0x0000000000000000-0x0000000000000000 r-xp 00:00 00 /usr/lib64/openfoam/20230801/libopenfoam.so看是一回事,看明白是另一回事,这里的地址都是虚拟地址,如果发生了miss,应该是发生了冲突(因为tlb miss不高)导致的缓存被invalid掉了。如何根据虚拟地址来计算出实际库存到的物理地址?这需要了解cache的映射方法。一般现在的cache都是VIPT,这种方法的特征是,如果使用的是4k内存页,那么虚拟地址的低12位和物理地址的低12位是相同的。取出地址的低12位,去掉表示偏移的低6位,计算表示index的6-11位。
可以发现,正常的进程的index是相同的,而异常进程的index是不同的。虽然动态库被多个进程共享,在内存中仅保留一份。但是core和core是物理隔离的,私有缓存中会各自存储一份。
但是根据问题表现,一个物理核上的两个超线程的任务性能一致,该问题和超线程还有关。核心支持超线程,L1 cache是共享的,所以超线程之间的cache会共享。但是index不一致也仅仅说明了两个超线程各自缓存了一份,并不会导致性能问题。
此时不应该将视角仅局限在libopenfoam.so库上,计算出其它链接库的index后,列表对比发现,性能正常的核心上,两个超线程的index是相同的,而异常核心上,两个超线程的index是不同的。那么就可以做出推测:
- 相同的动态库在cache中存储了两份,导致cache实际存储大小减半。但是减半仍不是核心问题;
- 一个核心上的两个超线程的动态库index发生了交叉式的冲突,读取一个动态库时会invalid另一个动态库的缓存,这才导致了cache miss异常高的情况。
为什么x86下无此问题?
该问题仅在arm环境下出现,而x86环境下无此问题。
x86是经典的4k内存页,L1 cache 32k,8-way相联,也就是每个way 4k。所以内存中的动态库地址映射到cache的index一定是相同的。两个进程共享一个位置的cache。
arm上也是4k页,但是L1 cache 64k,4-way相联,每way有16k。导致index多了2位,这样index的返回就超过了12位,而aslr随机的是虚拟地址,index的值也会随机化,进而导致了两个超线程“交叉冲突”的问题。
解决方案
这里寻思一下,其实是L1 cache的设计问题。硬件的设计已经固定,无法改变。所以只能从软件层来处理。目前测试可行的解决方法为:
- 可以使用64k的内存页,这样增加了两位物理地址映射的index,可以避免index随机化;
- 关闭ASLR;或使用
setarch -R来执行; - 关闭SMT;
延申
按照本文的分析,如果发生了cache alias,仅仅是性能变差了(高cache miss),而不是错误。显然是存在补救机制的。这里我倾向是硬件有检测机制,invalid掉了发生alias的缓存,因此导致了明显的性能损失。
另外,软件层面也有相关的避免alias的手段,叫page coloring,这里放一篇相关的论文:page coloring的历史与发展。但是从文中看出,这是一种纯软件的解决方案,且在现代处理器中已经不再使用,下为AI给出的:
在通用的桌面和服务器操作系统(如标准的 Linux 内核)中,针对 L1/L2 Cache 的传统 Page Coloring(页面着色)技术已经基本被边缘化或废弃了;但在特定领域(如嵌入式、实时系统、虚拟化隔离、以及科研/特定 Benchmark 调优)中,它依然以新的形态存在。 硬件上的改进:别名检测与全相联/高相联度 现代大核心 CPU(特别是 x86 架构如 Intel/AMD)的 L1 Cache 几乎都严格控制在“单个 Way 不超过 Page Size(4K)”的范围内(例如 32KB 8-Way 或 48KB 12-Way,每 Way 刚好 4K),从硬件源头上消除了 Alias 冲突。 而对于单 Way 超过 4K 的架构(如某些 Arm 核心,或者现代处理器的 L2/L3 大缓存),硬件内部普遍集成了别名检测逻辑(Alias Detection)或改用物理索引(PIPT)。虽然硬件检测会带来你所观察到的“微架构流水线停顿/icache miss 飙升”的性能副作用,但它保证了程序的正确性。 软件上的代价:Linus Torvalds 的经典反对理由 Linux 内核掌门人 Linus Torvalds 曾公开表达过对 Page Coloring 的极度厌恶("cache coloring definitely makes performance worse")。 内存碎片与分配压力:一旦操作系统引入 Page Coloring,意味着内存页面被强行划分成了不同的“颜色”区域。当某个特定颜色的页面耗尽时,即使系统还有大量其他颜色的空闲内存,分配器也无法使用,这会导致严重的内存碎片和内存级联压力。 实际效果得不偿失:在真实的多任务复杂应用场景中(而非单纯的线性流式 Benchmark),复杂的内存分配行为会迅速打破“着色”带来的收益,反而因为复杂的着色判断逻辑降低了内核分配内存的效率。 因此,现代 Linux 内核的伙伴系统(Page Allocator)默认不包含针对 L1/L2 Cache 的物理页面着色机制。