当前位置:编程学习 > C/C++ >>

C++游戏程序优化

 原 文:Optimlzation for C++ Games - Game Programming Gems II
  译 者:carvenson


--------------------------------------------------------------------------------

  一般而言,比起C程序来说,C++游戏程序是可重用和可维护的。可这真的有价值吗?复杂的C++可以在速度上与传统的C程序相提并论吗?
  如果有一个好的编译器,再加上对语言的了解,真的有可能用C++写出一些有效率的游戏程序来。本文描述了典型的几种你可以用来加速游戏的技术。它假设你已经非常肯定使用C++的好处,并且你也对优化的基本概念相当熟悉。
  第一个经常让人获益的基本概念显然是剖析(profiling)的重要性。缺乏剖析的话,程序员将犯两种错误,其一是优化了错误的代码:如果一个程序的主要指标不是效率,那么一切花在使其更高效上的时间都是浪费。靠直觉来判断哪段代码的主要指标是效率是不可信的,只有直接去测量。第二个概念是程序员经常“优化”到降低了代码的速度。这在C++是一个典型问题,一个简单的指令行可能会产生巨大数量的机器代码,你应当经常检查你的编译器的输出,并且剖析之。

1、对象的构造与析构
  对象的构造与析构是C++的核心概念之一,也是编译器背着你产生代码的一个主要地方。未经认真设计的程序经常花费不少时间在调用构造函数,拷贝对象以及初始化临时对象等等。幸运的是,一般的感觉和几条简单的规则可以让沉重的对象代码跑得和C只有毫厘之差。
  除非需要否则不构造。
  最快的代码是根本不运行的代码。为什么要创建一个你根本不去使用的对象呢?在后面的代码中:

  voide Function(int arg)
  {
    Object boj;
    If(arg==0)
      Return;
    ...
  }

  即便arg为0,我们也付出了调用Object的构造函数的代价。特别是如果arg经常是0,并且Object本身还分配内存,这种浪费会更加严重。显然的,解决方案就是把obj的定义移到判断之后。
  小心在循环中定义复杂变量,如果在循环中按照除非需要否则不构造的原则构造了复杂的对象,那么你在每一次循环的时候都要付出一次构造的代价。最好在循环外构造之以只构造一次。如果一个函数在内循环中被调用,而该函数在栈内构造了一个对象,你可以在外部构造并传递一个应用给它。

  1.1 采用初始化列表
  考虑下面的类:

  class Vehicle
  {
  public
    Vehicle(const std::string &name)
    {
      mName=name
    }
  private:
    std::string mName;
  }

  因为成员变量会在构造函数本体执行前构造,这段代码调用了string mName的构造函数,然后调用了一个=操作符,来拷贝其值。这个例子中的一个典型的不好之处在于string的缺省构造函数会分配内存,但实际上都会分配大大超过实际需要的空间。接下来的代码会好些,并且阻止了对=操作符的调用,进一步的来说,因为给出了更多的信息,非缺省构造函数会更有效,并且编译器可以在构造函数函数体为空的情况下将其优化掉。

  class Vehicle
  {
  public
    Vehicle(const std::string &name):mName(name)
    { }
  private:
    std::string mName;
  }

  1.2 要前自增不要后自增(即要++I不要I++)
  当写x=y++时产生的问题是自增功能将需要制造一个保持y的原值的拷贝,然后y自增,并把原始的值返回。后自增包括了一个临时对象的构造,而前自增则不要。对于整数,这没有额外的负担,但对于用户自定义类型,这就是浪费,你应该在有可能的情况下运用前自增,在循环变量中,你会常遇到这种情形。
  不使用有返回值的操作符 在C++中经常看到这样写顶点的加法:

  Vector operator+(const Vector &v1,const Vector &v2)

  这个操作将引起返回一个新的Vector对象,它还必须被以值的形式返回。虽然这样可以写v=v1+v2这样的表达式,但象构造临时对象和对象的拷贝这样的负担,对于象顶点加法这样常被调用的事情来说太大了一点。有时候是可以好好规划代码以使编译器可以把临时对象优化掉(这一点就是所谓的返回值优化)。但是更普遍的情形下,你最好放下架子,写一点难看但更快速的代码:

  void Vector::Add(const Vector &v1,const Vector &v2)

  注意+=操作符并没有同样的问题,它只是修改第一个参数,并不需要返回一个临时对象,所以,可能的情况下,你也可以用+=代替+。

  1.3 使用轻量级的构造函数
  在上一个例子中Vector的构造函数是否需要初始化它的元素为0?这个问题可能在你的代码中会有好几处出现。如果是的话,它使得无论是否必要,所有的调用都要付初始化的代价。典型的来说,临时顶点以及成员变量就会要无辜的承受这些额外的开销。
  一个好的编译器可以很好的移除一些这种多余的代码,但是为什么要冒这个险呢?作为一般的规则,你希望构造函数初始化所有的成员变量,因为未初始化的数据将产生错误。但是,在频繁实例化的小类中,特别是一些临时对象,你应该准备向效率规则妥协。首选的情况就是在许多游戏中有的vector和Matrix类,这些类显然应当提供一些方法置0和识别,但它的缺省构造函数却应当是空的。
  这个概念的推论就是你应当为这种类提供另一个构造函数。如果我们的第二个例子中的Vebicle类是这样写的话:

  class Vehicle
  {
  public:
    vehicle()
    {
    }
    void SetName(const std::string &name)
    {
      mName=name;
    }
  private:
    std::string mName
  };

  我们省去了构造mName的开销,而在稍后用SetName方法设置了其值。相似的,使用拷贝构造函数将比构造一个对象然后用=操作符要好一些。宁愿这样来构造:Vebicle V1(V2)也不要这样来构造:

  Vehicle v1;v1=v2;

  如果你需要阻止编译器帮你拷贝对象,把拷贝构造函数和操作符=声明为私有的,但不要实现其中任何一个。这样,任何企图对该对象的拷贝都将产生一个编译时错误。最好也养成定义单参数构造函数的习惯,除非你是要做类型转换。这样可以防止编译器在做类型转换时产生的隐藏的临时对象。

  1.4 预分配和Cache对象
  一个游戏一般会有一些类会频繁的分配和释放,比如武器什么的。在C程序中,你会分配一个大数组然后在需要的时候使用。在C++中,经过小小的规划以后,你也可以这样干。这个方法是不要一直构造和析构对象而是请求一个新而把旧的返回给Cache。Cache可以实现成一个模板,它就可以为所有的有一个缺省构造函数的类工作。Cache模板的Sample可以在附带的CD中找到。
  你也可以在需要时分配一些对象来填充Cache,或者预先分配好。如果你还要对这些对象维护一个堆栈的话(表示在你删除对象X之前,你先要删除所有在X后面分配的对象),你可以把Cache分配在一个连续的内存块中。

2、内存管理
  C++应用程序一般要比C程序更深入到内存管理的细节。在C中,所有的分配都简单的通过malloc和free来进行,而C++则还可以通过构造临时对象和成员变量来隐式的分配内存。很多C++游戏程序需要自己的内存管理程序。 由于C++游戏程序要执行很多的分配,所以要特别小心堆的碎片。一个方法是选择一条复杂的路:要么在游戏开始后根本不分配任何内存,要么维护一个巨大的连续内存块,并按期释放(比如在关卡之间)。在现代机器上,如果你想对你的内存使用很警惕的话,很严格的规则是没必要的。
  第一步是重载new和Delete操作符,使用自己实现的操作符来把游戏最经常的内存分配从malloc定向到预先分配好的内存块去,例如,你发现你任何时候最多有10000个4字节的内存分配,你可以先分配好40000字节,然后在需要时引用出来。为了跟踪哪些块是空的,可以维护一个由每一个空的块指向下一个空的块的列表free list。在分配的时候,把前面的block移掉,在释放的时候,把这个空块再放到前面去。图1描述了这个free list如何在一个连续的内存块中,与一系列的分配和释放协作的情形。

 

 
图1 A linked free list


  你可以很容易的发现一个游戏是有着许多小小的生命短暂的内存分配,你也许希望为很多小块保留空间。为那些现在没有使用到的东西保留大内存块会浪费很多内存。在一定的尺寸上,你应当把内存分配交给一支不同的大内存分配函数或是直接交给malloc()。

3、虚函数
  C++游戏程序的批评者总是把矛头对准虚函数,认为它是一个降低效率的神秘特性。概念性的说,虚函数的机制很简单。为了完成一个对象的虚函数调用,编译器访问对象的虚函数表,获得一个成员函数的指针,设置调用环境,然后跳转到该成员函数的地址上。相对于C程序的函数调用,C程序则是设置调用环境,然后跳转到一个既定的地址上。一个虚函数调用的额外负担是虚函数表的间接指向;由于事先并不知道将要跳转的地址,所以也有可能造成处理器不能命中Cache。
  所有真正的C++程序都对虚函数有大量的使用,所以主要的手段是防止在那些极其重视效率的地方的虚函数调用。这里有一个典型的例子:

  Class BaseClass
  {
  public:
    virtual char *GetPointer()=0;
  };

  Class Class1: public BaseClass
  {
    virtual char *GetPointer();
  };

  Class Class2:public BaseClass
  {
    virtual char *GetPointer();
  };

  void Function(BaseClass *pObj)
  {
    char *ptr=pObj->G

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,