最近在做系统调优,总结了下cache相关知识,以及如何提高性能和并发性能的方法。
一CACHE相关
1. cache概述
cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU访问主存的次数。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就不用再从主存中取出。
2. cache结构
假设内存容量为M,内存地址为m位:那么寻址范围为000…00~FFF…F(m位)
倘若把内存地址分为以下三个区间:
《深入理解计算机系统》p305 英文版 beta draft
tag, set index, block offset三个区间有什么用呢?再来看看Cache的逻辑结构吧:
将此图与上图做对比,可以得出各参数如下:
B = 2^b
S = 2^s
现在来解释一下各个参数的意义:
一个cache被分为S个组,每个组有E个cacheline,而一个cacheline中,有B个存储单元,现代处理器中,这个存储单元一般是以字节(通常8个位)为单位的,也是最小的寻址单元。因此,在一个内存地址中,中间的s位决定了该单元被映射到哪一组,而最低的b位决定了该单元在cacheline中的偏移量。valid通常是一位,代表该cacheline是否是有效的(当该cacheline不存在内存映射时,当然是无效的)。tag就是内存地址的高t位,因为可能会有多个内存地址映射到同一个cacheline中,所以该位是用来校验该cacheline是否是CPU要访问的内存单元。
当tag和valid校验成功是,我们称为cache命中,这时只要将cache中的单元取出,放入CPU寄存器中即可。
当tag或valid校验失败的时候,就说明要访问的内存单元(也可能是连续的一些单元,如int占4个字节,double占8个字节)并不在cache中,这时就需要去内存中取了,这就是cache不命中的情况(cache miss)。当不命中的情况发生时,系统就会从内存中取得该单元,将其装入cache中,与此同时也放入CPU寄存器中,等待下一步处理。注意,以下这一点对理解linux cache机制非常重要:
当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中。
例如:我们要取地址 (t, s, b) 内存单元,发生了cache miss,那么系统会取 (t, s, 00…000) 到 (t, s, FF…FFF)的内存单元,将其放入相应的cacheline中。
二 优化性能和提高并发性的主要方法我觉得有:
1.用设计上压缩结构体大小,使整个结构体能处于一个cache line中。
2.结构体中频繁访问的字段尽量放在一个结构体的同一个cache line里面,这样可以提高cache命中率。
3为了避免cache伪共享问题,cache伪共享多发生在多核cpu的情况,比如有4个cpu,出于并发的考虑,我们可能会设立一个全局数组struct cpu_info test[4];每个cpu去访问一个test数组元素,这样会产生cache共享问题,虽然各个cpu都修改各自的数据,但他们对数据的修改,会影响到彼此各个cpu的cache访问,这就是种伪共享,解决的方法是把数据结构struct cpu_info定义成cache line对齐,即加上____cacheline_aligned前缀。
4.频繁读写的多个结构体变量尽量同时申请,使得它们尽可能的分布在较小的线性空间范围内,这样可利用TLB缓冲,提高tlb的命中率。
5.当结构体比较大时,对结构体字段进行初始化或设置值时最好从第一个字段依次往后进行,这样可保证对内存的访问是顺序进行。这样可以充分的利用cpu的cache.
6.额外的优化可以采用非暂时移动指令(如movntdq)与预读指令(如prefetchnta)。比如网卡取数据包时,在取到一个数据包进行处理时,可以使用prefech指令去取接下来的一个数据包,从而达到提高并发能力的效果。不过使用prefech指令时,最好有足够的时间让其能并发的取到,否则得不偿失,反而降低了设备性能。
7.特殊情况可考虑利用多媒体指令SSE2、SSE4等。
8memset与memecpy对于是非常耗费cpu资源的,在有可能的情况下,尽量避免对大数据结构进行memset操作,可以只对某几个字段进行赋值处理。若在必须要做memset()的情况下,可以自己开发个优化的版本,比如对于64位的机器,原始的memset()函数(来自内核2.6.18):
static inline void * __memset_generic(void * s, char c,size_t count)
{
int d0, d1;
__asm__ __volatile__(
"rep\n\t"
"stosb"
: "=&c" (d0), "=&D" (d1)
:"a" (c),"1" (s),"0" (count)
:"memory");
return s;
}
该memset()函数其实就是个循环写,每次写一个字节,慢的很。我们可以为64位开发个新的版本:
static inline void layout_memset_64(void *address, int len)
{
__u64 *addr = (__u64 *)address;
int div = len >> 6;/* len / 64 */
int md = len % 64;
int n = div - 1;
int ni = n<<3;/* n*8 */
int i;
for (i = 0; i <= ni; i += 8){
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
*addr++ = 0;
}
div = md >> 3;
md %= 8;
for (i = 0; i < div; i++)
*addr++ = 0;
for (i = 0; i < md; i++){
*((__u8*)addr+i) = 0;
}
return;
}
对于64位的平台来说,每次写可以写入8个字节的数据,循环比上面的函数快了很多,会很大程度提高系统性能。
9使用大页内存,尽量提高tlb命中率,即cpu中进行地址转换的快表。大页内存一个物理页大小为4m bytes,远大于普通的4k的物理页面大小。
10对于提高多核系统的并发性,主要的调优手段主要有尽可能缩小互斥区,缩小锁的粒度,最好能避免并发的情况,比如采用类似linux 内核中per cpu的方式,同时采用并发性比较好的锁,如读写锁,互斥锁。对于一些网络设备,可以把数据会话固定分担到各个cpu,从而避免并发。