1.尽量避免线程之间共享变量,如果需要共享,变量结构定义为Cache line对齐。
Cache取数据是按照cache line为单位(我们的系统下64Byte),数据跨越两个cache line,就意味着两次load或者两次store。如果数据结构是cache line对齐的,就有可能减少一次读写。数据结构的首地址cache line对齐,意味着可能有内存浪费(特别是数组这样连续分配的数据结构),所以需要在空间和时间两方面权衡。
例如a[8],然后每个线程访问固定一个下标来访问,并且存在写操作,这样会使效率暴降,除非把a的数据类型是按CPU的cache line大小定义的。
2. 频繁更新和不频繁更新的数据分开存放,读数据和写数据分开。实际操作具有一定的难度。
3.数据的分布尽可能的按照访问顺序定义。实际操作具有一定的难度。
4.减少使用全局变量,增强数据访问的局部性。
程序的运行存在时间和空间上的局部性,前者是指只要内存中的值被换入缓存,今后一段时间内会被多次引用,后者是指该内存附近的值也被换入缓存。函数操作数据最好都在栈上,因为局部性的数据很可能被载入到cache了,频繁访问局部性数据也会使得对应的cache数据不被频繁替换出cache(在64位机器上,局部变量会优先放到寄存器中),而全局变量存储在全局数据段,在一个被反复调用的函数体内,引用该变量需要对缓存多次换入换出。
循环体内的代码要尽量精简,因为代码(就是指令?)是放在L1指令缓存(I-Cache)里的,而L1指令缓存只有几K字节大小,如果对某段代码需要多次读取,而这段代码又跨越一个L1缓存大小,那么缓存优势将荡然无存。
5.增强数据访问的局部性也是减少cache miss 的最直接方法,编译器所作的优化很多也只是在不改变程序逻辑的情况下增加数据访问局部性。但编译器这方面能做的很有限。
6.数据访问遵循cache预取的时间局部性和空间局部性。
Cache取数据是按照cache line为单位,当读取内存中某个地址数据时,该地址附近的数据也被一起换入cache,所以程序访问数据时也要遵循此规则。cache的命中率对多层循环的影响是最明显的,因此在设计循环逻辑的时候,如果有某个数据结构需要多次访问,尽量让其全部在最里层中完成访问。还有遍历二维数组时要先遍历二维下标等。
7.在写多核程序时,尽量绑定线程到一个cpu上。因为不同cpu之间L1, L2 cache是独立的。
8.减少函数指针使用。
编译器完全看不出函数指针指向的规律,分支预测器对这种分支地址预测也是两眼一抹黑,所以要尽量避免用函数指针。如果一个应用程序超过几M大小而且还会调用一些动态库,这样会造成函数指针在很广阔的空间飞来飞去,会导致cache命中率大幅降低,但是没有太好的办法。
