C++代码优化

 
谈到优化,很多人都会直接想到汇编。难道优化只能在汇编层次吗?当然不是,C++层次一样可以作代码优化,其中有些常常是意想不到的。在C++层次进行优化,比在汇编层次优化具有更好的移植性,应该是优化中的首选做法。
1.确定浮点型变量和表达式是 float 型
为了让编译器产生更好的代码(比如说产生3DNow! 或SSE指令的代码),必须确定浮点型变量和表达式是 float 型的。要特别注意的是,以 ";F"; 或 ";f"; 为后缀(比如:3.14f)的浮点常量才是 float 型,否则默认是 double 型。为了避免 float 型参数自动转化为 double,请在函数声明时使用 float。
2.使用32位的数据类型
  编译器有很多种,但它们都包含的典型的32位类型是:int,signed,signed int,unsigned,unsigned int,long,signed long,long int,signed long int,unsigned long,unsigned long int。尽量使用32位的数据类型,因为它们比16位的数据甚至8位的数据更有效率。
3.明智使用有符号整型变量
  在很多情况下,你需要考虑整型变量是有符号还是无符号类型的。比如,保存一个人的体重数据时不可能出现负数,所以不需要使用有符号类型。但是,如果是要保存温度数据,就必须使用到有符号的变量。
  在许多地方,考虑是否使用有符号的变量是必要的。在一些情况下,有符号的运算比较快;但在一些情况下却相反。
  比如:整型到浮点转化时,使用大于16位的有符号整型比较快。因为x86构架中提供了从有符号整型转化到浮点型的指令,但没有提供从无符号整型转化到浮点的指令。看看编译器产生的汇编代码:
  不好的代码:
编译前 编译后
double x; mov [foo + 4], 0
unsigned int i; mov eax, i
x = i; mov [foo], eax
flid qword ptr [foo]
fstp qword ptr [x]
  上面的代码比较慢。不仅因为指令数目比较多,而且由于指令不能配对造成的FLID指令被延迟执行。最好用以下代码代替:
推荐的代码:
编译前 编译后
double x; fild dword ptr
int i; fstp qword ptr [x]
x = i;
  在整数运算中计算商和余数时,使用无符号类型比较快。以下这段典型的代码是编译器产生的32位整型数除以4的代码:
  不好的代码
编译前 编译后
int i; mov eax, i
i = i / 4; cdq
and edx, 3
add eax, edx
sar eax, 2
mov i, eax
推荐的代码
编译前 编译后
unsigned int i; shr i, 2
i = i / 4;
 总结:
 无符号类型用于:除法和余数,循环计数,数组下标
有符号类型用于:整型到浮点的转化
4.while VS. for
  在编程中,我们常常需要用到无限循环,常用的两种方法是while (1) 和 for (;;)。这两种方法效果完全一样,但那一种更好呢?然我们看看它们编译后的代码:
编译前 编译后
while (1); mov eax,1
test eax,eax
je foo+23h
jmp foo+18h
编译前 编译后
for (;;); jmp foo+23h
  一目了然,for (;;)指令少,不占用寄存器,而且没有判断跳转,比while (1)好。
5.使用数组型代替指针型
  使用指针会使编译器很难优化它。因为缺乏有效的指针代码优化的方法,编译器总是假设指针可以访问内存的任意地方,包括分配给其他变量的储存空间。所以为了编译器产生优化得更好的代码,要避免在不必要的地方使用指针。一个典型的例子是访问存放在数组中的数据。C++ 允许使用操作符 [] 或指针来访问数组,使用数组型代码会让优化器减少产生不安全代码的可能性。比如,x[0] 和x[2] 不可能是同一个内存地址,但 *p 和 *q 可能。强烈建议使用数组型,因为这样可能会有意料之外的性能提升。
不好的代码
typedef struct
{
  float x,y,z,w;
} VERTEX;
typedef struct

{
  float m[4][4];
} MATRIX;
void XForm(float* res, const float* v, const float* m, int nNumVerts)
{
  float dp;
  int i;
   const VERTEX* vv = (VERTEX *)v;
   for (i = 0; i <; nNumVerts; i++)
  {
    dp = vv->;x * *m ++;
    dp += vv->;y * *m ++;
    dp += vv->;z * *m ++;
    dp += vv->;w * *m ++;
    *res ++ = dp;      // 写入转换了的 x
    dp = vv->;x * *m ++;
    dp += vv->;y * *m ++;
    dp += vv->;z * *m ++;
    dp += vv->;w * *m ++;
    *res ++ = dp;     // 写入转换了的 y
    dp = vv->;x * *m ++;
    dp += vv->;y * *m ++;
    dp += vv->;z * *m ++;
    dp += vv->;w * *m ++;
    *res ++ = dp;    // 写入转换了的 z
    dp = vv->;x * *m ++;
    dp += vv->;y * *m ++;
    dp += vv->;z * *m ++;
    dp += vv->;w * *m ++;
    *res ++ = dp;    // 写入转换了的 w
    vv ++;        // 下一个矢量
    m -= 16;
  }
}
推荐的代码
typedef struct
{
  float x,y,z,w;
} VERTEX;
typedef struct
{
  float m[4][4];
} MATRIX;
void XForm (float* res, const float* v, const float* m, int nNumVerts)
{
  int i;
  const VERTEX* vv = (VERTEX*)v;
  const MATRIX* mm = (MATRIX*)m;
  VERTEX* rr = (VERTEX*)res;
  for (i = 0; i <; nNumVerts; i++)
  {
    rr->;x = vv->;x * mm->;m[0][0] + vv->;y * mm->;m[0][1]
        + vv->;z * mm->;m[0][2] + vv->;w * mm->;m[0][3];
    rr->;y = vv->;x * mm->;m[1][0] + vv->;y * mm->;m[1][1]
        + vv->;z * mm->;m[1][2] + vv->;w * mm->;m[1][3];
    rr->;z = vv->;x * mm->;m[2][0] + vv->;y * mm->;m[2][1]
        + vv->;z * mm->;m[2][2] + vv->;w * mm->;m[2][3];
    rr->;w = vv->;x * mm->;m[3][0] + vv->;y * mm->;m[3][1]
        + vv->;z * mm->;m[3][2] + vv->;w * mm->;m[3][3];
  }
}
  注意: 源代码的转化是与编译器的代码发生器相结合的。从源代码层次很难控制产生的机器码。依靠编译器和特殊的源代码,有可能指针型代码编译成的机器码比同等条件下的数组型代码运行速度更快。明智的做法是在源代码转化后检查性能是否真正提高了,再选择使用指针型还是数组型。
6.充分分解小的循环
  要充分利用CPU的指令缓存,就要充分分解小的循环。特别是当循环体本身很小的时候,分解循环可以提高性能。BTW:很多编译器并不能自动分解循环。
不好的代码 推荐的代码
// 3D转化:把矢量 V 和 4x4 矩阵 M 相乘
for (i = 0; i <; 4; i ++)
{
  r = 0;
  for (j = 0; j <; 4; j ++)
  {
    r += M[j]*V[j];
  }
}
r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];
r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];
r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];
r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];
7.避免没有必要的读写依赖
  当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。虽然AMD Athlon等CPU有加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,但是,如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避免读写依赖。所以推荐程序员手动去消除读写依赖,举例来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子:
不好的代码
float x[VECLEN], y[VECLEN], z[VECLEN];
......
for (unsigned int k = 1; k <; VECLEN; k ++)
{
  x[k] = x[k-1] + y[k];
}
for (k = 1; k <; VECLEN; k++)
{
  x[k] = z[k] * (y[k] - x[k-1]);
}
   推荐的代码
float x[VECLEN], y[VECLEN], z[VECLEN];
......
float t(x[0]);
for (unsigned int k = 1; k <; VECLEN; k ++)
{
  t = t + y[k];
  x[k] = t;
}
t = x[0];
for (k = 1; k <; VECLEN; k ++)
{
  t = z[k] * (y[k] - t);
  x[k] = t;
}
8.Switch 的用法
  Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表和比较链/树。推荐对case的值依照发生的可能性进行排序,把最有可能的放在第一个,当switch用比较链的方式转化时,这样可以提高性能。此外,在case中推荐使用小的连续的整数,因为在这种情况下,所有的编译器都可以把switch 转化成跳转表。
不好的代码
int days_in_month, short_months, normal_months, long_months;
......
switch (days_in_month)
{
  case 28:
  case 29:
    short_months ++;
    break;
  case 30:
    normal_months ++;
    break;
  case 31:
    long_months ++;
    break;
  default:
    cout <;<; ";month has fewer than 28 or more than 31 days"; <;<; endl;
    break;
}
推荐的代码
int days_in_month, short_months, normal_months, long_months;
......
switch (days_in_month)
{
  case 31:
    long_months ++;
    break;
  case 30:
    normal_months ++;
    break;
  case 28:
  case 29:
    short_months ++;
    break;
  default:
    cout <;<; ";month has fewer than 28 or more than 31 days"; <;<; endl;
    break;
}
9.所有函数都应该有原型定义
  一般来说,所有函数都应该有原型定义。原型定义可以传达给编译器更多的可能用于优化的信息。
  尽可能使用常量(const)。C++ 标准规定,如果一个const声明的对象的地址不被获取,允许编译器不对它分配储存空间。这样可以使代码更有效率,而且可以生成更好的代码。
10.提升循环的性能
  要提升循环的性能,减少多余的常量计算非常有用(比如,不随循环变化的计算)。
  不好的代码(在for()中包含不变的if()) 推荐的代码
for( i ... )
{
  if( CONSTANT0 )
  {
    DoWork0( i ); // 假设这里不改变CONSTANT0的值
  }
  else
  {
    DoWork1( i ); // 假设这里不改变CONSTANT0的值
  }
}
if( CONSTANT0 )
{
  for( i ... )
  {
    DoWork0( i );
  }
}
else
{
  for( i ... )
  {
    DoWork1( i );
  }
}
  如果已经知道if()的值,这样可以避免重复计算。虽然不好的代码中的分支可以简单地预测,但是由于推荐的代码在进入循环前分支已经确定,就可以减少对分支预测的依赖。   把本地函数声明为静态的(static)
  如果一个函数在实现它的文件外未被使用的话,把它声明为静态的(static)以强制使用内部连接。否则,默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化——比如,自动内联。
11.考虑动态内存分配
  动态内存分配(C++中的";new";)可能总是为长的基本类型(四字对齐)返回一个已经对齐的指针。但是如果不能保证对齐,使用以下代码来实现四字对齐。这段代码假设指针可以映射到 long 型。
  例子
  double* p = (double*)new BYTE[sizeof(double) * number_of_doubles+7L];
double* np = (double*)((long(p) + 7L) &; –8L);
  现在,你可以使用 np 代替 p 来访问数据。注意:释放储存空间时仍然应该用delete p。
12.使用显式的并行代码
  尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。因为浮点操作有很长的潜伏期,所以不管它被映射成 x87 或 3DNow! 指令,这都很重要。很多高级语言,包括C++,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。需要注意的是,重排序的代码和原来的代码在代数上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。
  不好的代码
double a[100], sum;
int i;
sum = 0.0f;
for (i=0; i<;100; i++)
  sum += a;
推荐的代码
double a[100], sum1, sum2, sum3, sum4, sum;
int i;
sum1 = sum2 = sum3 = sum4 = 0.0;
for (i = 0; i <; 100; i += 4)
{
  sum1 += a;
  sum2 += a[i+1];
  sum3 += a[i+2];
  sum4 += a[i+3];
}
sum = (sum4+sum3)+(sum1+sum2);
  要注意的是:使用4 路分解是因为这样使用了4阶段流水线浮点加法,浮点加法的每一个阶段占用一个时钟周期,保证了最大的资源利用率。
13.提出公共子表达式
  在某些情况下,C++编译器不能从浮点表达式中提出公共的子表达式,因为这意味着相当于对表达式重新排序。需要特别指出的是,编译器在提取公共子表达式前不能按照代数的等价关系重新安排表达式。这时,程序员要手动地提出公共的子表达式(在VC.net里有一项“全局优化”选项可以完成此工作,但效果就不得而知了)。
推荐的代码
float a, b, c, d, e, f;
...
e = b * c / d;
f = b / d * a;
float a, b, c, d, e, f;
...
const float t(b / d);
e = c * t;
f = a * t;
推荐的代码
float a, b, c, e, f;
...
e = a / c;
f = b / c;
float a, b, c, e, f;
...
const float t(1.0f / c);
e = a * t;
f = b * t;
14.结构体成员的布局
  很多编译器有“使结构体字,双字或四字对齐”的选项。但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是,有些编译器并不提供这些功能,或者效果不好。所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐,建议采取这些方法:
  A按类型长度排序
  把结构体的成员按照它们的类型长度排序,声明成员时把长的类型放在短的前面。
  把结构体填充成最长类型长度的整倍数
  把结构体填充成最长类型长度的整倍数。照这样,如果结构体的第一个成员对齐了,所有整个结构体自然也就对齐了。下面的例子演示了如何对结构体成员进行重新排序:
  不好的代码,普通顺序 推荐的代码,新的顺序并手动填充了几个字节
struct
{
  char a[5];
  long k;
  double x;
} baz;
struct
{
  double x;
  long k;
  char a[5];
char pad[7];
} baz;

  这个规则同样适用于类的成员的布局。
  B按数据类型的长度排序本地变量
  当编译器分配给本地变量空间时,它们的顺序和它们在源代码中声明的顺序一样,和上一条规则一样,应该把长的变量放在短的变量前面。如果第一个变量对齐了,其它变量就会连续的存放,而且不用填充字节自然就会对齐。有些编译器在分配变量时不会自动改变变量顺序,有些编译器不能产生4字节对齐的栈,所以4字节可能不对齐。下面这个例子演示了本地变量声明的重新排序:
  不好的代码,普通顺序 推荐的代码,改进的顺序
short ga, gu, gi;
long foo, bar;
double x, y, z[3];
char a, b;
float baz;
double z[3];
double x, y;
long foo, bar;
float baz;
short ga, gu, gi;
14.避免不必要的整数除法
  整数除法是整数运算中最慢的,所以应该尽可能避免。一种可能减少整数除法的地方是连除,这里除法可以由乘法代替。这个替换的副作用是有可能在算乘积时会溢出,所以只能在一定范围的除法中使用。
  不好的代码 推荐的代码
int i, j, k, m;
m = i / j / k;
int i, j, k, m;
m = i / (j * k);
15.把频繁使用的指针型参数拷贝到本地变量
  避免在函数中频繁使用指针型参数指向的值。因为编译器不知道指针之间是否存在冲突,所以指针型参数往往不能被编译器优化。这样是数据不能被存放在寄存器中,而且明显地占用了内存带宽。注意,很多编译器有“假设不冲突”优化开关(在VC里必须手动添加编译器命令行/Oa或/Ow),这允许编译器假设两个不同的指针总是有不同的内容,这样就不用把指针型参数保存到本地变量。否则,请在函数一开始把指针指向的数据保存到本地变量。如果需要的话,在函数结束前拷贝回去。   
不好的代码
// 假设 q != r
void isqrt(unsigned long a, unsigned long* q, unsigned long* r)
{
  *q = a;
  if (a >; 0)
  {
    while (*q >; (*r = a / *q))
    {
      *q = (*q + *r) >;>; 1;
    }
  }
  *r = a - *q * *q;
}
推荐的代码
// 假设 q != r
void isqrt(unsigned long a, unsigned long* q, unsigned long* r)
{
  unsigned long qq, rr;
  qq = a;
  if (a >; 0)
  {
    while (qq >; (rr = a / qq))
    {
      qq = (qq + rr) >;>; 1;
    }
  }
  rr = a - qq * qq;
  *q = qq;
  *r = rr;
}
16.赋值与初始化
先看看以下代码:
class CInt
{
  int m_i;
public:
  CInt(int a = 0):m_i(a) { cout <;<; ";CInt"; <;<; endl; }
  ~CInt() { cout <;<; ";~CInt"; <;<; endl; }
  CInt operator + (const CInt&; a) { return CInt(m_i + a.GetInt()); }
  void SetInt(const int i)  { m_i = i; }
  int GetInt() const      { return m_i; }
};
不好的代码
void main()
{
  CInt a, b, c;
  a.SetInt(1);
  b.SetInt(2);
  c = a + b;
}
推荐的代码
void main()
{
  CInt a(1), b(2);
  CInt c(a + b);
}
  这两段代码所作的事都一样,但那一个更好呢?看看输出结果就会发现,不好的代码输出了四个";CInt";和四个";~CInt";,而推荐的代码只输出三个。也就是说,第二个例子比第一个例子少生成一次临时对象。Why? 请注意,第一个中的c用的是先声明再赋值的方法,第二个用的是初始化的方法,它们有本质的区别。第一个例子的";c = a + b";先生成一个临时对象用来保存a + b的值,再把该临时对象用位拷贝的方法给c赋值,然后临时对象被销毁。这个临时对象就是那个多出来的对象。第二个例子直接用拷贝构造函数的方法对c初始化,不产生临时对象。所以,尽量在需要使用一个对象时才声明,并用初始化的方法赋初值。
17.尽量使用成员初始化列表
  在初始化类的成员时,尽量使用成员初始化列表而不是传统的赋值方式。
  不好的代码
class CMyClass
{
  string strName;
public:
  CMyClass(const string&; str);
};
CMyClass::CMyClass(const string&; str)
{
  strName = str;
}
推荐的代码
class CMyClass
{
  string strName;
  int i;
public:
  CMyClass(const string&; str);
};
CMyClass::CMyClass(const string&;str)
:strName(str)
{

}
  不好的例子用的是赋值的方式。这样,strName会先被建立(调用了string的默认构造函数),再由参数str赋值。而推荐的例子用的是成员初始化列表,strName直接构造为str,少调用一次默认构造函数,还少了一些安全隐患。

  编写高效简洁的C语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。

  第一招:以空间换时间

  计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决问题的第1招--以空间换时间。

  例如:字符串的赋值。

  方法A:通常的办法:

#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,"This is a example!!");


  方法B:

const char string2[LEN] ="This is a example!";
char * cp;
cp = string2 ;


  使用的时候可以直接用指针来操作。

  从上面的例子可以看出,A和B的效率是不能比的。在同样的存储空间下,B直接使用指针就可以操作了,而A需要调用两个字符函数才能完成。B的缺点在于灵活性没有A好。在需要频繁更改一个字符串内容的时候,A具有更好的灵活性;如果采用方法B,则需要预存许多字符串,虽然占用了大量的内存,但是获得了程序执行的高效率。

 如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。该招数的变招--使用宏函数而不是函数。举例如下:

  方法C:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK(int __bf)
{
 return ((1U << (bw ## __bf)) - 1) << (bs ## __bf);
}
void SET_BITS(int __dst, int __bf, int __val)
{
 __dst = ((__dst) & ~(BIT_MASK(__bf))) | /
 (((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))
}

SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);


  方法D:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)
#define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf))
#define SET_BITS(__dst, __bf, __val) /
((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | /
(((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))

SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);


  函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。

  D方法是我看到的最好的置位操作函数,是ARM公司源码的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有的位操作功能。C方法是其变体,其中滋味还需大家仔细体会。

第二招:数学方法解决问题

  现在我们演绎高效C语言编写的第二招--采用数学方法来解决问题。数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序的执行效率有数量级的提高。举例如下,求 1~100的和。

  方法E

int I , j;
for (I = 1 ;I<=100; I ++)
{
 j += I;
}


  方法F

int I;
I = (100 * (1+100)) / 2


  这个例子是我印象最深的一个数学用例,是我的计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用公式 N×(N+1)/ 2 来解决这个问题。方法E循环了100次才解决问题,也就是说最少用了100个赋值,100个判断,200个加法(I和j);而方法F仅仅用了1个加法,1次乘法,1次除法。效果自然不言而喻。所以,现在我在编程序的时候,更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。

  第三招:使用位操作

  实现高效的C语言编写的第三招——使用位操作。减少除法和取模的运算。在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用"位运算"来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例如下:

  方法G

int I,J;
I = 257 /8;
J = 456 % 32;


  方法H

int I,J;
I = 257 >>3;
J = 456 - (456 >> 4 << 4);

在字面上好像H比G麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法G调用了基本的取模函数和除法函数,既有函数调用,还有很多汇编代码和寄存器参与运算;而方法H则仅仅是几句相关的汇编,代码更简洁,效率更高。当然,由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 来看,效率的差距还是不小。相关汇编代码就不在这里列举了。

  运用这招需要注意的是,因为CPU的不同而产生的问题。比如说,在PC上用这招编写的程序,并在PC上调试通过,在移植到一个16位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。

  第四招:汇编嵌入

  高效C语言编程的必杀技,第四招——嵌入汇编。"在熟悉汇编语言的人眼里,C语言编写的程序都是垃圾"。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方法 --嵌入汇编,混合编程。举例如下,将数组一赋值给数组二,要求每一字节都相符。

char string1[1024],string2[1024];


  方法I

int I;
for (I =0 ;I<1024;I++)
 *(string2 + I) = *(string1 + I)


  方法J

#ifdef _PC_
int I;
for (I =0 ;I<1024;I++)
*(string2 + I) = *(string1 + I);
#else
#ifdef _ARM_
__asm
{
 MOV R0,string1
 MOV R1,string2
 MOV R2,#0
loop:
 LDMIA R0!, [R3-R11]
 STMIA R1!, [R3-R11]
 ADD R2,R2,#8
 CMP R2, #400
 BNE loop
}
#endif


  方法I是最常见的方法,使用了1024次循环;方法J则根据平台不同做了区分,在ARM平台下,用嵌入汇编仅用128次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为0的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于LCD数据的拷贝过程。根据不同的CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。

  虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在不同平台移植的过程中,卧虎藏龙,险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才可以采用。切记,切记。
C语言中变量的位置与程序优化
前几天一个朋友在网上问我:“怎么区分全局变量,静态变量和自动变量?”我觉得奇怪,他怎么问一个初学者的问题,我这位朋友其实编程挺厉害的,呵呵。接着他说是:“计算机怎么区分”。确实,人区分各种变量一看就知道了,但是计算机怎么区分呢?没有编译知识可能一下子也难弄懂。


计算机理解力是没法和我们相比的,但是它比我们快。我们可以把这些变量分开来放,从不同地方取来的就是不同的变量。告诉计算机去找的就是符号表。当然,生成符号表是编译器的事,我们不用管。但是了解一点总是有好处的。这里我以Unix ELF(Executalbe and Linkable Format)格式文件为例说明编译器是怎么安排全局变量,静态变量和自动变量的位置的。
ELF可重定位目标文件包括:ELF头以及.text,.rodata, .data ,.bss ,.symtab, .rel.text, .rel.data 等节。
全局变量:已初始化的保存在.data段中 ,未初始化的表示为.bss段的一个占位符;
静态变量:根据是否初始化分别在.data 和.bss段中分配空间;
自动变量(非静态局部变量):在运行时保存在栈中。既不在.data 段中也不在.bss段中。
其实我们可以编写一个简单的程序,编译了以后用objdump命令查看各种变量所在的节。


知道了各种变量在运行时的位置也就知道了他们的储存期,而且了解变量的位置对编写高性能程序也非常有帮助。
我们知道嵌入式对程序的性能要求是非常高的,函数的参数列表越长那么函数调用的开销就越大,这个时候我们可以使用全局变量提高程序的性能。
将函数和变量声明为static可防止函数和变量被其它模块不正确的使用。
关于自动变量,请看下面的两个程序:
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值较大的时候,上面两个程序的性能是有显著差别的。 这是利用了程序的局部性原理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值