目前CPU运行速度远超过内存访问速度,且从趋势看这种速度差距还会越拉越大,提高内存访问效率将是软件优化重要而长期的课题。内存访问优化的一般性措施可大体分两方面:1)减少内存访问;2)调整代码使程序集中顺序地访问内存。
一、减少内存访问的措施包括:
a.充分利用寄存器
充分利用寄存器缓存数据,是减少内存访问的思路之一。C程序编译后哪些元素由寄存器存储,哪些又会放进内存,取决于CPU以及对应的编译器规范。以ARM为例,对于遵循ATPCS规则的编译器:
1)函数前4个参数放在寄存器里,超出4个则压入栈内存。
2)局部变量如果没有取址操作,或有取址但未赋给其他变量,就会被编译器优先安排寄存器存储,如寄存器已占完,则开始在寄存器和栈之间交换存储。
不同CPU及编译器有类似规范,注意下面几点能更充分地利用寄存器:
1)如果函数参数过多,把多个参数组织成结构体,传递结构体指针。在很多平台上,这样能减少参数入栈的几率。
2)用register提示编译器把关键变量或循环内变量用寄存器缓存,register暗示编译器变量将被频繁使用,应将其保存在寄存器中,以加快其存储速度,但要注意它仅仅是个提示,很多时候编译器并不鸟它。
3)把某些大函数拆分成小函数,防止因寄存器不足导致局部变量在栈和寄存器之间反复存取,类似内存和硬盘间的内容交换,浪费时间。大函数内局部变量多,情况复杂,编译器无法分析清每个局部变量的作用范围,常常做出很多无用的压栈出栈操作。
4)如某热点函数内经常访问全局变量,可添加一个临时局部变量,诱导编译器将该全局变量内容读到寄存器中作为其影子,对寄存器进行相关操作,最后赋回全局变量,以减少内存访问。如:
long product;
void factorialA(long n)
{
long i;
for(i = 1; i <= n;i++ ){ product *= i; }
}
void factorialB(long n)
{
long i
long x = 1;
for(i = 1; i <= n;i++ ){ x *= i; }
product = x;
}
n较大时,上面两个函数性能有显著差别,这就是充分利用寄存器的好处。
5)避免局部变量取址, 编译器处理局部变量时一般先尽量用通用寄存器缓存,但如果有局部变量取址操作,意味着该变量只能放在栈内存(通用寄存器没有内存地址概念)。如果该局部变量在循环中多次读写,此时也同样可考虑增加中间变量,用完后再写回。比如下例改动就能提高整体效率:
void f(int *a);
int g(int a);
int test1(int i)
{
int j;
f(&i);
for(j =0;j<1000;j++)
i += g(i);
return i;
}
修改后
int test2(int i)
{
int temp = i;
f(&temp);
i = temp;
for(j =0;j<1000;j++)
i += g(i);
return i;
}
test2中使用了变量的拷贝temp,把temp的地址传入函数f(),函数f()退出时再把temp回赋给i,这样变量i不存在取址操作,编译器就能把它用寄存器保存。这里循环内对i有数千次访问,循环体中的i放在寄存器相比放在栈内存,效率差别相当大。
b.消除指针链
访问多级结构体成员变量时常要使用指针链,如:
typedef struct { int x, y, z; }point;
typedef struct {point *pos, *direct; }obj;
void InitPos(obj *p)
{
p->pos->x = 0;
p->pos->y = 0;
p->pos->z = 0;
}
如果编译器不能确定p->pos->x不是p->pos的别名,代码中每次赋值操作都要重新访问p->pos(了解下restrict)。所以最好是手动把p->pos存到一个局部变量,改为:
void InitPos(obj *p)
{
point *pos = p->pos;
pos->x = 0;
pos->y = 0;
pos->z = 0;
}
这样只需一次p->pos内存访问,比之前省了两次。而且这不是节省两条普通指令,而是两条访问随机内存的指针链操作。
二、集中连续访问内存包括:
a.合理安排和调整循环次序
循环中的内存访问多数都是性能热点,是连续集中还是断续分散访问,很大程度影响系统性能。有时仅仅调整循环次序,使分散内存访问变得连续,就能大幅提升性能。如:
for(i=0;i<N;i++)
for(j=0;j<N;j++)
A(j,i) = B(j,i) + C(j,i) * D
变为:
for(j=0;j<N;j++)
for(i=0;i<N;i++)
A(j,i) = B(j,i) + C(j,i) * D
交换后, A, B, C均按其在内存中的排列顺序依次被访问,而不是像之前那样跳跃式访问。访问内存就象逛街购物,好不容易出来一次,自然要尽量把需要的东西都买回去,否则又要多跑,这中间需要时间代价。
b.使用连续内存的数据结构
链表结构相比数组,对内存的占用更少且更灵活,但在内存访问密集型的应用中却会导致性能的明显下降,因为访问链表节点是分散随机地访问内存。与之对应访问数组内存则相对集中且连续,所以如果对链表的随机访问成为性能阻碍,不妨考虑用数组代替链表,或者从预分配的大块连续内存上分配链表节点,而不是用malloc随机申请内存块。注意:无论cache或non-cache系统中,把离散内存访问变为连续内存访问都能提高系统性能。至于如何写出cache-friendly的代码则是更高级的主题。
以上只是从内存角度出发,C语言级优化的几个基本着眼点,仅为大家抛砖引玉。