C优化篇之优化内存访问

    目前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随机申请内存块。注意:无论cachenon-cache系统中,把离散内存访问变为连续内存访问都能提高系统性能。至于如何写出cache-friendly的代码则是更高级的主题。

 

以上只是从内存角度出发,C语言级优化的几个基本着眼点,仅为大家抛砖引玉。

展开阅读全文

没有更多推荐了,返回首页