预取
正如前面对CPU缓存的介绍,对于存放在内存中的数据,CPU处理相关数据一般需要先从内存取数据到L3,再从L3取数据到L2,再从L2取数据到L1,最后将L1中的数据取到寄存器中,这时候CPU才能对相关数据进行处理。如果CPU下一次需要处理的数据都在L1中,显然这样程序的性能会优于数据都在内存中。而对于预取来说,又可以分为硬件预取和软件预取两种。硬件预取指的由硬件根据访存的历史信息,对未来可能的访存单元预先取入Cache,从而在数据真正被用到时不会造成Cache失效,具有通用性。而软件预取是指程序员通过在业务代码中编写预取指令,对特定位置进行预取,具有针对性。
本文只要介绍基于鲲鹏平台进行编程,故只介绍软件预取。
软件预取
软件预取是通过预取指令实现的,不同架构提供的预取指令也不一样。在鲲鹏平台上,预取指令格式通常如下:
PRFM prfop, [Xn|SP{, #pimm}]
prfop由type<target><policy>三部分组成。
- type可选模式有:
- PLD:数据预加载
- PLI:指令预取
- PST:数据预存储
- <target>可选模式有:
- L1
- L2
- L3
分别表示对三个不同的Cache层级进行操作。
- <policy>可选模式有:
- KEEP:数据预取使用后保存一定时间,适用于数据多次使用的场景。
- STRM:流式或非临时预取,数据使用后将淘汰,用于仅使用一次的数据。
- Xn|SP:通常表示64位通用寄存器或栈指针,使用场景中通常为预取的起始地址。
- pimm:以字节为单位的偏移量,取值为8的整数倍,范围是0到32760,默认为0。
从指令组成看,预取指令中核心部分为prfop,其决定了预取的类型、预取cache层级以及预取的数据使用模式。本小节主要说明PLD数据预取,其他模式类似,数据预取核心指令部分有以下几种使用方式。
数据预取指令 |
指令功能说明 |
---|---|
PLDL1KEEP |
数据预取到L1 cache,策略为keep模式,数据使用后常驻Cache。 |
PLDL2KEEP |
数据预取到L2 cache,策略为keep模式,数据使用后常驻Cache。 |
PLDL3KEEP |
数据预取到L3 cache,策略为keep模式,数据使用后常驻Cache。 |
PLDL1STRM |
数据预取到L1 cache,策略为strm模式,数据使用后从Cache淘汰。 |
PLDL2STRM |
数据预取到L2 cache,策略为strm模式,数据使用后从Cache淘汰。 |
PLDL3STRM |
数据预取到L3 cache,策略为strm模式,数据使用后从Cache淘汰。 |
GCC编译器针对预取也有对应的builtin函数实现,格式如下:
__builtin_prefetch (const void *addr, int rw, int locality)
其中:
- addr表示数据的内存地址。
- rw为可选参数,rw可设置为0或1,0表示读操作,1表示写操作。
- locality为可选参数,可设置0~3,表示数据在cache中保持的时间,即时效性。取值为0表示访问的数据后续不再被访问,使用后在cache中淘汰;取值为3表示访问的数据将再次访问;取值1和2则分别表示具有低时效性和中时效性,该值默认为3。
未使用软件预取:
int add_vector(int *dst, int *src1, int *src2, int size) { for (int index =0 ; index < size; index += 4) { … // do something } }
使用软件预取:
static inline void prefetch(const void* data) { __asm__ __volatile__( "prfm PLDL1STRM, [%[data]] \n\t" :: [data] "r" (data)); } int add_vector(int *dst, int *src1, int *src2, int size) { for (int index =0 ; index < size; index += 4) { prefetch(src1 + 256); prefetch(src2 + 256); … // do something } }