内存子系统是现代计算机系统中的一个主要性能瓶颈,降低内存延迟和所需的吞吐量以提高性能至关重要。在计算机系统中,CPU高速缓存(Cache)处于金字塔式存储体系自顶向下的第二层,仅次于CPU寄存器,容量远小于内存,但速度可以接近CPU的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在,则不经访问内存直接返回该数据;如果不存在,则要先把内存中的相应数据载入缓存,再将其返回处理器。有效利用Cache可以极大提高程序的访存效率,降低系统的内存瓶颈,而程序运行时是否能有效利用缓存,取决于访存的局部性特征。如何通过各种优化手段,高效利用内存局部性,提升系统性能对编译器来说是一个巨大的挑战。
1 结构体内存布局优化
结构体内存布局优化基于全程序(Whole-program)优化,用以提高缓存(cache)利用率。优化的主要手段是将结构体数组转换为数组结构体,结构体可以是显式的,也可以通过检查循环中的数组使用情况来推断它们。基本原理如图1所示:

图1 结构体内存优化原理示意图
2 数组重排列优化
和结构体类似,数组也可以重新排列(重新映射),将非连续的数组访问转换成连续的数组访问。

图2 数组重排列优化原理示意图
性能测试,LBM(Lattice Boltzmann Method) 鲲鹏920 1P/2P环境下测试效果:
毕昇编译器经过array remapping优化后,在1P(64 copies) 和 2P(128 copies)场景下,性能均能提升1倍以上。

图3 数组重排列优化在LBM算法上的优化效果
3 软件预取增强
软件预取是提高Cache利用率的重要手段。鲲鹏处理器提供了软件预取指令,编译器可以显式生成预取指令,指定需要预取的数据,从而将所需数据提前从内存读入Cache。如果编译器能够准确计算需要预取的数据地址,基本保证每次CPU需要读取数据时,数据刚好通过预取指令读入Cache,则访存的延时可以大大降低。但是预取指令本身的执行会带来额外的开销,如果不能在正确的时间发出预取指令,或者预取的内存地址与实际需求相差很远,可能会导致Cache污染,反而降低系统的性能。因此编译器生成软件预取指令的能力对生成高性能的二进制至关重要。
毕昇编译器通过与鲲鹏处理器协同,将硬件相关特征准确建模,使得编译器预取分析相关代码能够准确模拟鲲鹏处理器的访存特征,再通过循环、数组等典型内存访问场景的分析增强,从而准确生成对目标内存的预取指令,提升程序运行性能。
如上所示代码,经过编译器分析后,可以插入如下的预取指令:
以上代码中的a[i+k]为预取的数据,k代表提前多少次迭代预取所需的数据。这一数字取决于底层硬件的访存延时、Cache line大小等等。通过对鲲鹏处理器相关参数的分析与建模,毕昇编译器可以针对类似场景生成准确的预取指令。
同时,LLVM原生的软件预取分析能力有限,例如当循环的步长不是编译时常量时,无法做出预取分析,这会导致有些场景无法生成预取指令。毕昇编译器对此做了增强,使得软件预取可以在复杂循环场景下完成分析,从而覆盖更多的内存访问场景。
软件预取对SPEC CPU2017,多个HPC Workload的性能都有大幅提升。
内存子系统是现代计算机系统中的一个主要性能瓶颈,降低内存延迟和所需的吞吐量以提高性能至关重要。在计算机系统中,CPU高速缓存(Cache)处于金字塔式存储体系自顶向下的第二层,仅次于CPU寄存器,容量远小于内存,但速度可以接近CPU的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在,则不经访问内存直接返回该数据;如果不存在,则要先把内存中的相应数据载入缓存,再将其返回处理器。有效利用Cache可以极大提高程序的访存效率,降低系统的内存瓶颈,而程序运行时是否能有效利用缓存,取决于访存的局部性特征。如何通过各种优化手段,高效利用内存局部性,提升系统性能对编译器来说是一个巨大的挑战。
1 结构体内存布局优化
结构体内存布局优化基于全程序(Whole-program)优化,用以提高缓存(cache)利用率。优化的主要手段是将结构体数组转换为数组结构体,结构体可以是显式的,也可以通过检查循环中的数组使用情况来推断它们。基本原理如图1所示:
图1 结构体内存优化原理示意图
2 数组重排列优化
和结构体类似,数组也可以重新排列(重新映射),将非连续的数组访问转换成连续的数组访问。
图2 数组重排列优化原理示意图
性能测试,LBM(Lattice Boltzmann Method) 鲲鹏920 1P/2P环境下测试效果:
毕昇编译器经过array remapping优化后,在1P(64 copies) 和 2P(128 copies)场景下,性能均能提升1倍以上。
图3 数组重排列优化在LBM算法上的优化效果
3 软件预取增强
软件预取是提高Cache利用率的重要手段。鲲鹏处理器提供了软件预取指令,编译器可以显式生成预取指令,指定需要预取的数据,从而将所需数据提前从内存读入Cache。如果编译器能够准确计算需要预取的数据地址,基本保证每次CPU需要读取数据时,数据刚好通过预取指令读入Cache,则访存的延时可以大大降低。但是预取指令本身的执行会带来额外的开销,如果不能在正确的时间发出预取指令,或者预取的内存地址与实际需求相差很远,可能会导致Cache污染,反而降低系统的性能。因此编译器生成软件预取指令的能力对生成高性能的二进制至关重要。
毕昇编译器通过与鲲鹏处理器协同,将硬件相关特征准确建模,使得编译器预取分析相关代码能够准确模拟鲲鹏处理器的访存特征,再通过循环、数组等典型内存访问场景的分析增强,从而准确生成对目标内存的预取指令,提升程序运行性能。
for (int i = 0; i < N; i++) { sum += a[i] * b[i]; }如上所示代码,经过编译器分析后,可以插入如下的预取指令:
for (int i = 0; i < N; i++) { prefetch(&a[i+k]); prefetch(&b[i+k]); sum += a[i] * b[i]; }以上代码中的a[i+k]为预取的数据,k代表提前多少次迭代预取所需的数据。这一数字取决于底层硬件的访存延时、Cache line大小等等。通过对鲲鹏处理器相关参数的分析与建模,毕昇编译器可以针对类似场景生成准确的预取指令。
同时,LLVM原生的软件预取分析能力有限,例如当循环的步长不是编译时常量时,无法做出预取分析,这会导致有些场景无法生成预取指令。毕昇编译器对此做了增强,使得软件预取可以在复杂循环场景下完成分析,从而覆盖更多的内存访问场景。
软件预取对SPEC CPU2017,多个HPC Workload的性能都有大幅提升。