关于C 代码优化的方法总结

原文来自:http://www.west263.com/info/html/chengxusheji/C-C--/20080224/13128_2.html

优化是个很大的主题,本文并不是去深入探讨性能分析理论,算法的效率,况且我也没有这个能力。我只是想把一些能够简单的应用到您的C 代码中的优化技术总结在这里,这样,当您碰到几种不同的编程策略的时候,就能够对每种策略的性能进行一个大概的估计。这也是本文的目的之所在。


   一. 优化之前

  在进行优化之前,我们首先应该做的是发现我们代码的瓶颈(bottleneck)在哪里。然而当您做这件事情的时候切忌从一个debug-version进行推断,因为debug-version中包含了许多额外的代码。一个debug-version可执行体要比release-version大出40%。那些额外的代码都是用来支持调试的,比如说符号的查找。大多数实现都为debug-version和release-version提供了不同的operatorew连同库函数。而且,一个release-version的执行体可能已通过多种途径进行了优化,包括不必要的临时对象的消除,循环展开,把对象移入寄存器,内联等等。

  另外,我们要把调试和优化区分开来,他们是在完成不同的任务。 debug-version是用来追捕bugs连同检查程式是否有逻辑上的问题。release-version则是用来做一些性能上的调整连同进行优化。

  下面就让我们来看看有哪些代码优化技术吧:

   二. 声明的放置

  程式中变量和对象的声明放在什么位置将会对性能产生显著影响。同样,对ostfix和prefix运算符的选择也会影响性能。这一部分我们集中讨论四个问题:初始化v.s 赋值,在程式确实要使用的地方放置声明,构造函数的初始化列表,refix v.s postfix运算符。

  (1) 请使用初始化而不是赋值

  在C语言中只允许在一个函数体的开头进行变量的声明,然而在C 中声明能够出现在程式的任何位置。这样做的目的是希望把对象的声明拖延到确实要使用他的时候再进行。这样做能够有两个好处:1. 确保了对象在他被使用前不会被程式的其他部分恶意修改。假如对象在开头就被声明然而却在20行以后才被使用的话,就不能做这样的确保。2. 使我们有机会通过用初始化取代赋值来达到性能的提升,从前声明只能放在开头,然而往往开始的时候我们还没有获得我们想要的值,因此初始化所带来的好处就无法被应用。但是现在我们能够在我们获得了想要的值的时候直接进行初始化,从而省去了一步。注意,或许对于基本类型来说,初始化和赋值之间可能不会有什么差异,但是对于用户定义的类型来说,二者就会带来显著的不同,因为赋值会多进行一次函数调用----operator =。因此当我们在赋值和初始化之间进行选择的话,初始化应该是我们的最好选择。

  (2) 把声明放在合适的位置上

  在一些场合,通过移动声明到合适的位置所带来的性能提升应该引起我们足够的重视。例如:

  ool is_C_Needed();

  void use()

  {

  C c1;

  if (is_C_Needed() == false)

  {

  return; //c1 was not needed

  }

  //use c1 here

  return;

  }

  上面这段代码中对象c1即使在有可能不使用他的情况下也会被创建,这样我们就会为他付出不必要的花费,有可能您会说一个对象c1能浪费多少时间,但是假如是这种情况呢:C c1[1000];我想就不是说浪费就浪费了。但是我们能够通过移动声明c1的位置来改变这种情况:

  void use()

  {

  if (is_C_Needed() == false)

  {

  return; //c1 was not needed

  }

  C c1; //moved from the block's beginning

  //use c1 here

  return;

  }

  怎么样,程式的性能是不是已得到很大的改善了呢?因此请仔细分析您的代码,把声明放在合适的位置上,他所带来的好处是您难以想象的。

  (3) 初始化列表

  我们都知道,初始化列表一般是用来初始化const或reference数据成员。但是由于他自身的性质,我们能够通过使用初始化列表来实现性能的提升。我们先来看一段程式:

  class Person

  {

  rivate:

  C c_1;

  C c_2;

  ublic:

  Person(const C& c1, const C& c2 ): c_1(c1), c_2(c2) {}

  };

  当然构造函数我们也能够这样写:

  Person::Person(const C& c1, const C& c2)

  {

  c_1 = c1;

  c_2 = c2;

  }

  那么究竟二者会带来什么样的性能差异呢,要想搞清楚这个问题,我们首先要搞清楚二者是如何执行的,先来看初始化列表:数据成员的声明操作都是在构造函数执行之前就完成了,在构造函数中往往完成的只是赋值操作,然而初始化列表直接是在数据成员声明的时候就进行了初始化,因此他只执行了一次copy constructor。再来看在构造函数中赋值的情况:首先,在构造函数执行前会通过defaultconstructor创建数据成员,然后在构造函数中通过operator =进行赋值。因此他就比初始化列表多进行了一次函数调用。性能差异就出来了。但是请注意,假如您的数据成员都是基本类型的话,那么为了程式的可读性就不要使用初始化列表了,因为编译器对两者产生的汇编代码是相同的。

  (4) postfix VS prefix 运算符

  refix运算符 和―比他的postfix版本效率更高,因为当postfix运算符被使用的时候,会需要一个临时对象来保存改变以前的值。对于基本类型,编译器会消除这一份额外的拷贝,但是对于用户定义类型,这似乎是不可能的。因此请您尽可能使用prefix运算符。

   三. 内联函数

  内联函数既能够去除函数调用所带来的效率负担又能够保留一般函数的长处。然而,内联函数并不是万能药,在一些情况下,他甚至能够降低程式的性能。因此在使用的时候应该慎重。

  1.我们先来看看内联函数给我们带来的好处:从一个用户的角度来看,内联函数看起来和普通函数相同,他能够有参数和返回值,也能够有自己的作用域,然而他却不会引入一般函数调用所带来的负担。另外,他能够比宏更安全更容易调试。

当然有一点应该意识到,inline specifier仅仅是对编译器的建议,编译器有权利忽略这个建议。那么编译器是如何决定函数内联和否呢?一般情况下关键性因素包括函数体的大小,是否有局部对象被声明,函数的复杂性等等。

  2.那么假如一个函数被声明为inline但是却没有被内联将会发生什么呢?理论上,当编译器拒绝内联一个函数的时候,那个函数会像普通函数相同被对待,但是还会出现一些其他的问题。例如下面这段代码:

  // filename Time.h

  #include<ctime>

  #include<iostream>

  using namespace std;

  class Time

  {

  ublic:

  inline void Show() { for (int i = 0; i<10; i )

  cout<<time(0)<<endl;}

  };

  因为成员函数Time::Show()包括一个局部变量和一个for循环,所以编译器一般拒绝inline,并且把他当作一个普通的成员函数。但是这个包含类声明的头文档会被单独的#include进各个单独的编译单元中:

  // filename f1.cpp

  #include "Time.hj"

  void f1()

  {

  Time t1;

  t1.Show();

  }

  // filename f2.cpp

  #include "Time.h"

  void f2()

  {

  Time t2;

  t2.Show();

  }

  结果编译器为这个程式生成了两个相同成员函数的拷贝:

  void f1();

  void f2();

  int main()

  {

  f1();

  f2();

  return 0;

  }

  当程式被链接的时候,linker将会面对两个相同的Time::Show()拷贝,于是函数重定义的连接错误发生。但是老一些的C 实现对付这种情况的办法是通过把一个un-inlined函数当作static来处理。因此每一份函数拷贝仅仅在自己的编译单元中可见,这样链接错误就解决了,但是在程式中却会留下多份函数拷贝。在这种情况下,程式的性能不但没有提升,反而增加了编译和链接时间连同最终可执行体的大小。

  但是幸运的是,新的C 标准中关于un-inlined函数的说法已改变。一个符合标准C 实现应该只生成一份函数拷贝。然而,要想任何的编译器都支持这一点可能还需要很长时间。

  另外关于内联函数更有两个更令人头疼的问题。第一个问题是该如何进行维护。一个函数开始的时候可能以内联的形式出现,但是随着系统的扩展,函数体可能需要

  添加额外的功能,结果内联函数就变得不太可能,因此需要把inline specifier去除连同把函数体放到一个单独的源文档中。另一个问题是当内联函数被应用在代码库的时候产生。当内联函数改变的时候,用户必须重新编译他们的代码以反映这种改变。然而对于一个非内联函数,用户仅仅需要重新链接就能够了。

  这里想要说的是,内联函数并不是个增强性能的灵丹妙药。只有当函数很短小的时候他才能得到我们想要的效果,但是假如函数并不是很短而且在很多地方都被调用的话,那么将会使得可执行体的体积增大。最


特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有。 
本站文章均来自网络,如有侵权,请联系028-86262244-200 QQ: 1585463984 我们将立即删除!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值