程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved