• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            實時陰影繪制技術研究

            C++博客 首頁 新隨筆 聯系 聚合 管理
              48 Posts :: 20 Stories :: 57 Comments :: 0 Trackbacks
            作者:amure
            原文地址:http://www.azure.com.cn/article.asp?id=190

            一般而言,比起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
            [img]http://http://www.linuxeden.com/edu/mays.soage.com/develop/optimize/200112/image/Other/OptFORCGame.gif[/img]

              你可以很容易的發現一個游戲是有著許多小小的生命短暫的內存分配,你也許希望為很多小塊保留空間。為那些現在沒有使用到的東西保留大內存塊會浪費很多內存。在一定的尺寸上,你應當把內存分配交給一支不同的大內存分配函數或是直接交給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->GetPointer();
              }

              如果Function()極其重視效率,我們應當把GetPointer從一個虛函數改成內聯函數。一種方式是給BaseClass增加一個新的保護的數據成員,在每一個類中設置該成員的值,在GetPointer這個內聯函數中返回該成員給調用者:

              Class BaseClass
              {
              public:
                inline char GetPointerFast()
                {
                  return mpPointer;
                }
              protected:
                inline void SetPointer(char *pData)
                {
                  mpData = pData;
                }
              private:
                char *mpData;
              };

              void Function(BaseClass *pObj)
              {
                char *ptr= pObj->GetPointerFast();
              }

              一個更激進的方法是重新規劃你的類繼承樹,如果Class1和Class2只有一點點不同,那么可以把它們捆綁到同一個類中去,而用一個Flag來表明它將象Class1還是象Class2一樣工作,同時在BaseClass中把純虛函數去掉。這樣的話,也可以象前面的例子一樣把GetPointer寫成內聯。這種變通看起來不是很高雅,但是在缺少Cache的機器上跑內循環時,你可能會很愿意為了去掉虛函數調用而把事情做得更加難看。
              雖然每一個新的虛函數都只給每個類的虛表增加了一個指針的尺寸(通常是可以忽略的代價),第一個虛函數還是在每一個對象上要求了一個指向虛表的指針。這就是說你在很小的、頻繁使用的類上使用任何虛函數而造成了額外的負擔,這些都是不能接受的。由于繼承一般都要用到一個或幾個虛函數(至少有一個虛的析構函數),所以你沒必要在小而頻繁使用的對象上使用任何繼承。

            4、代碼尺寸
              編譯器因為C++產生冗長的代碼而臭名昭著。由于內存有限,而小的東西往往是快的,所以使你的可執行文件盡可能的小是非常重要的。首先可以做的事情是拿一個編譯器來研究。如果你的編譯器會在可執行文件中保存Debug信息的話,那么把它們移除掉。(注意MS VC會把Debug信息放在可執行文件外部,所以沒關系)異常處理會產生額外的代碼,盡可能的去除異常處理代碼。確保連接器配置為去除無用的函數和類。開啟編譯器的最高優化級別,并嘗試設置為尺寸最小化而不是速度最大化 — 有時候,由于Cache命中的提高,會產生更好的運行效果。(注意在使用這項設置時檢查instrinsic功能是否也處于打開狀態)去掉所有Debug輸出狀態下的浪費空間的字符串,使編譯器把多個相同的字符串捆綁成一個實例。
              內聯通常是造成大函數的首犯。編譯器可以自由的選擇注意或忽略你寫的inline關鍵字,而且它們還會背著你制造一些內聯。這是另一個要你保持輕量級的構造函數的原因,這樣堆棧中的對象就不會因為有大量的內聯代碼而膨脹。同時也要小心重載運算符,即使是最簡短的表達式如m1=m2*m3如果m2和m3是矩陣的話,也可能產生大量的內聯代碼。一定要深入了解你的編譯器對于內聯的設置。
              啟用運行時類型信息(RTTI)需要編譯器為每一個類產生一些靜態信息。RTTI一般來說是缺省啟用的,這樣我們的代碼可以調用dynamic_cast以及檢測一個對象的類型,考慮完全禁止使用RTTI和dynamic_cast以節省空間(進一步的說,有時候dynamic_cast在某些實現中需要付出很高的代價)另一方面,當你真的需要有基于類型的不同行為的時候,增加一個不同行為的虛函數。這是更好的面向對象設計(注意static_cast與這不同,它的效率和C語言的類型轉換一樣)。

            5、標準類庫(STL)
              標準類庫是一套實現了常見的結構和算法的模板,例如dynamic arrays(稱為vector),set,map等等。使用STL可以節省你很多時間來寫和調試那些容器。和之前談到的一樣,如果希望系統的效率最大化,你必須要注意你的STL的具體實現的細節。
              為了能夠對應于最大范圍的應用,STL標準在內存分配這個領域保持了沉默。在STL容器中的每一個操作都有一定的效率保證,例如,給一個set進行插入操作只要O(log n)的時間,但是,對一個容器的內存使用沒有任何保證。
              讓我們來仔細了解游戲開發中的一個非常普遍的問題:你希望保存一組對象,(我們會稱其為對象列表,雖然不一定要保存在STL的列表中)通常你會要求每個對象在這個表有且僅有一個,這樣你就不用擔心一個偶然產生的在容器中插入一個已存在單元的操作了。STL的set忽略副本,所有的插入、刪除和查詢的速度都是O(log n),這是不是就是很好的選擇呢?
              雖然在set上的大多數操作的速度都是O(log n),但是這里面依然存在著潛在的危機。雖然容器的內存使用依賴于實現,但很多實現還是在紅黑樹的基礎上實現的。在紅黑樹上,樹的每一個節點都是容器的一個元素。常見的實現方法是在每一個元素被加入到樹時,分配一個節點,而當每個元素被移出樹時,釋放一個節點。根據你插入和刪除的頻繁程度,在內存管理器上所花費的時間將或多或少的影響你通過使用set而獲得的好處。
              另外一個解決方案是使用vector來存儲元素,vector保證在容器的末端添加元素有很高的效率。這表示實際上vector只在很偶然的情況下才重新分配內存,也就是說,當滿的時候擴容一倍。當使用vector來保存一個不同元素列表的時候,你首先要檢查元素是否已經存在,如果沒有,那么加入。而對整個vector檢查一遍需要花費O(n)的時間,但是但實際牽涉到的部分應該比較少,這是因為vector的每個元素都在內存中連續存放,所以檢查整個vector實際上是一個易于cache的操作。檢查整個set將造成cache不命中,這是因為在紅黑樹上分別存放的元素可能散布在內存的各個角落。同時,我們也注意到set必須額外維護一組標記以設置整個樹。如果你要保存的是對象的指針,set可能要花費vector所要花費的內存的3到4倍。
              Set的刪除操作消耗時間O(log n),看起來是很快,如果你不考慮可能對free()的調用的話。Vector的刪除操作消耗O(n),這是因為從被刪除的那個元素開始到結尾處的元素,每一個元素都要被拷貝到前一個位置上。如果元素都只是指針的話,那么這個拷貝將可以依靠一個簡單的memcpy()來完成,而這個函數是相當快的。(這也是為什么通常都把對象的指針儲存在STL的容器中的一個原因,而不是儲存對象本身。如果你直接保存了對象本身,將會在很多操作中造成許多額外的構造函數的調用,例如刪除等)。
              set和map通常來說麻煩大于有用,如果你還沒有意識到這一點的話,考慮遍歷一個容器的代價,例如:

              for(Collection::iterator it = Collection.begin(); it != Collection.end(); ++it)

              如果Collection是vector,那么++it就是一個指針自增。但是當Collection是一個set或者是一個map的話,++it包括了訪問紅黑樹上的下一個節點。這個操作相當復雜而且很容易造成cache不命中,因為樹的節點幾乎遍布內存的各處。
              當然,如果你要在容器中保存大量的元素,并且進行許多的成員請求,那么set的O(log n)的效率完全可以抵消那些內存方面的消耗。近似的,如果你偶爾才使用容器,那么這里的效率差別就非常的小。你應該做一些效率評估以了解多大的n會使set變得更快。也許你會驚奇的發現在游戲的大多數典型應用下vector的所有效率都比set要高。
              這還不是STL內存使用的全部。一定要了解當你使用clear方法時,容器是否真的釋放掉了它的內存。如果沒有,就可能產生內存碎片。比如,如果你開始游戲的時候建立了一個空的vector,在游戲過程中增加元素,然后在游戲restart時調用clear,這時vector未必釋放它的全部內存。這個空的vector,可能依然占據了堆中的內存,并使其變成碎片。如果你真的需要這樣來實現游戲的話,對這個問題有兩種解法。一是你可以在創建vector時調用reserve(),為你可能需要的最大數量的元素保留足夠的空間。如果這不可行的話,你可以強迫vector完全釋放內存:

              Vector V;
              // … elements are inserted into V here
              Vector().swap(v);  // causes v to free its memory

              Set、list以及map都沒有這個問題,這是因為他們為每個元素分別分配和釋放內存。

            6、高級特性
              編程語言的某些特性你可能沒必要用到。看上去簡單的特性可能會導致低下的效率。而看起來復雜的特性沒準執行得很好。C++的這些黑暗角落異常依賴于編譯器。當你要使用它們時,必須了解它們的代價。
              C++的string就是一個看起來不錯的例子,但是在效率極其重要的場合應該避免使用,考慮下面的代碼。

              Void Function(const std::string &str)
              {
              }
              Function("hello");

              對Function()的調用包括了對給定const char*參數的構造函數的調用。在普遍的實現中,這個構造函數執行了一個malloc(),一個strlen(),以及一個memcpy(),而析構函數立刻上來做了一些無意義的事情。(由于該例子中的string沒有被更多的應用)然后又跟了一個free()。這里的內存分配完全是浪費,因為字符串“hello”早就在程序的數據段中了。我們早就有它在內存中的副本了。如果Function定義了一個const char*的參數,那么完全沒有了上面所說的那些額外的調用。這就是為了使用方便的string而付出的高昂代價。
              模板是效率的對立面的一個例子,根據語言標準,編譯器在模板實例化為一個具體的類型時產生代碼。理論上,看上去是聲明了一個模板,但卻實際產生了大量的相似的代碼。如果你有了class1的指針的vector,也有class2的指針的vector,你就在你的可執行文件中做了兩份的vector的拷貝。
              事實上,大多數的編譯器做得更好,首先,只有實際被使用到的模板成員函數被產生代碼。其次,如果事先了解了正確的行為,編譯器可以只產生一份代碼的拷貝。你可以從vector的例子發現這一點,確實只產生了一份代碼(一般是vector)。有了好的編譯器,模板還是可以在保持高效的同時提供你一般編程的好處。
              C++的一些特性,比如初始化列表以及前自增,一般來說可以提高效率。而象其它的一些特性比如運算符重載以及RTTI則看起來似乎是清白的,但卻有時帶來嚴重的效率問題。STL的容器則描述了盲目相信函數的算法運行時間可以如何讓你誤入歧途。避免使用潛在的低效率的語言或類庫特性,同時花些時間來了解你的編譯器的各種選項。你會很快的學會設計高效的代碼,并且解決掉你的游戲中的效率問題。

            談到優化,很多人都會直接想到匯編。難道優化只能在匯編層次嗎?當然不是,C++層次一樣可以作代碼優化,其中有些常常是意想不到的。在C++層次進行優化,比在匯編層次優化具有更好的移植性,應該是優化中的首選做法。
              確定浮點型變量和表達式是 float 型

              為了讓編譯器產生更好的代碼(比如說產生3DNow! 或SSE指令的代碼),必須確定浮點型變量和表達式是 float 型的。要特別注意的是,以 ";F"; 或 ";f"; 為后綴(比如:3.14f)的浮點常量才是 float 型,否則默認是 double 型。為了避免 float 型參數自動轉化為 double,請在函數聲明時使用 float。

              使用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位的數據更有效率。

              明智使用有符號整型變量

              在很多情況下,你需要考慮整型變量是有符號還是無符號類型的。比如,保存一個人的體重數據時不可能出現負數,所以不需要使用有符號類型。但是,如果是要保存溫度數據,就必須使用到有符號的變量。

              在許多地方,考慮是否使用有符號的變量是必要的。在一些情況下,有符號的運算比較快;但在一些情況下卻相反。

              比如:整型到浮點轉化時,使用大于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 [i]
            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;

              總結:
               無符號類型用于:
               除法和余數
               循環計數
               數組下標
               有符號類型用于:
               整型到浮點的轉化
               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)好。

            使用數組型代替指針型

              使用指針會使編譯器很難優化它。因為缺乏有效的指針代碼優化的方法,編譯器總是假設指針可以訪問內存的任意地方,包括分配給其他變量的儲存空間。所以為了編譯器產生優化得更好的代碼,要避免在不必要的地方使用指針。一個典型的例子是訪問存放在數組中的數據。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];
               }
            }

              注意: 源代碼的轉化是與編譯器的代碼發生器相結合的。從源代碼層次很難控制產生的機器碼。依靠編譯器和特殊的源代碼,有可能指針型代碼編譯成的機器碼比同等條件下的數組型代碼運行速度更快。明智的做法是在源代碼轉化后檢查性能是否真正提高了,再選擇使用指針型還是數組型。

            [充分分解小的循環]

              要充分利用CPU的指令緩存,就要充分分解小的循環。特別是當循環體本身很小的時候,分解循環可以提高性能。BTW:很多編譯器并不能自動分解循環。

            不好的代碼 推薦的代碼

            // 3D轉化:把矢量 V 和 4x4 矩陣 M 相乘
            for (i = 0; i <; 4; i ++)
            {
               r[i] = 0;
               for (j = 0; j <; 4; j ++)
               {
                 r[i] += M[j][i]*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];

              [避免沒有必要的讀寫依賴]

              當數據保存到內存時存在讀寫依賴,即數據必須在正確寫入后才能再次讀取。雖然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;
            }

              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;
            }

              所有函數都應該有原型定義

              一般來說,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用于優化的信息。

              [盡可能使用常量(const)]

              盡可能使用常量(const)。C++ 標準規定,如果一個const聲明的對象的地址不被獲取,允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以生成更好的代碼。

            提升循環的性能

              要提升循環的性能,減少多余的常量計算非常有用(比如,不隨循環變化的計算)。

              不好的代碼(在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)以強制使用內部連接。否則,默認的情況下會把函數定義為外部連接。這樣可能會影響某些編譯器的優化——比如,自動內聯。

              考慮動態內存分配

              動態內存分配(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。

              使用顯式的并行代碼

              盡可能把長的有依賴的代碼鏈分解成幾個可以在流水線執行單元中并行執行的沒有依賴的代碼鏈。因為浮點操作有很長的潛伏期,所以不管它被映射成 x87 或 3DNow! 指令,這都很重要。很多高級語言,包括C++,并不對產生的浮點表達式重新排序,因為那是一個相當復雜的過程。需要注意的是,重排序的代碼和原來的代碼在代數上一致并不等價于計算結果一致,因為浮點操作缺乏精確度。在一些情況下,這些優化可能導致意料之外的結果。幸運的是,在大部分情況下,最后結果可能只有最不重要的位(即最低位)是錯誤的。

              不好的代碼 推薦的代碼

            double a[100], sum;
            int i;
            sum = 0.0f;
            for (i=0; i<;100; i++)
               sum += a[i];

            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[i];
               sum2 += a[i+1];
               sum3 += a[i+2];
               sum4 += a[i+3];
            }
            sum = (sum4+sum3)+(sum1+sum2);

              要注意的是:使用4 路分解是因為這樣使用了4階段流水線浮點加法,浮點加法的每一個階段占用一個時鐘周期,保證了最大的資源利用率。

            提出公共子表達式

              在某些情況下,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;

              結構體成員的布局

              很多編譯器有“使結構體字,雙字或四字對齊”的選項。但是,還是需要改善結構體成員的對齊,有些編譯器可能分配給結構體成員空間的順序與他們聲明的不同。但是,有些編譯器并不提供這些功能,或者效果不好。所以,要在付出最少代價的情況下實現最好的結構體和結構體成員對齊,建議采取這些方法:

              按類型長度排序

              把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。

              把結構體填充成最長類型長度的整倍數

              把結構體填充成最長類型長度的整倍數。照這樣,如果結構體的第一個成員對齊了,所有整個結構體自然也就對齊了。下面的例子演示了如何對結構體成員進行重新排序:

              不好的代碼,普通順序 推薦的代碼,新的順序并手動填充了幾個字節

            struct
            {
               char a[5];
               long k;
               double x;
            } baz;
            struct
            {
               double x;
               long k;
               char a[5];
            char pad[7];
            } baz;

              這個規則同樣適用于類的成員的布局。

              按數據類型的長度排序本地變量

              當編譯器分配給本地變量空間時,它們的順序和它們在源代碼中聲明的順序一樣,和上一條規則一樣,應該把長的變量放在短的變量前面。如果第一個變量對齊了,其它變量就會連續的存放,而且不用填充字節自然就會對齊。有些編譯器在分配變量時不會自動改變變量順序,有些編譯器不能產生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;

              避免不必要的整數除法

              整數除法是整數運算中最慢的,所以應該盡可能避免。一種可能減少整數除法的地方是連除,這里除法可以由乘法代替。這個替換的副作用是有可能在算乘積時會溢出,所以只能在一定范圍的除法中使用。

              不好的代碼 推薦的代碼

            int i, j, k, m;
            m = i / j / k;
            int i, j, k, m;
            m = i / (j * k);

              把頻繁使用的指針型參數拷貝到本地變量

              避免在函數中頻繁使用指針型參數指向的值。因為編譯器不知道指針之間是否存在沖突,所以指針型參數往往不能被編譯器優化。這樣是數據不能被存放在寄存器中,而且明顯地占用了內存帶寬。注意,很多編譯器有“假設不沖突”優化開關(在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;
            }

            賦值與初始化
            先看看以下代碼:

            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初始化,不產生臨時對象。所以,盡量在需要使用一個對象時才聲明,并用初始化的方法賦初值。

              盡量使用成員初始化列表

              在初始化類的成員時,盡量使用成員初始化列表而不是傳統的賦值方式。

              不好的代碼 推薦的代碼

            籧lass 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,少調用一次默認構造函數,還少了一些安全隱患。
            posted on 2006-03-18 00:03 苦行僧 閱讀(1412) 評論(2)  編輯 收藏 引用 所屬分類: 轉載

            Feedback

            # re: C++ 代碼優化 2006-08-06 20:59 jiffwan
            樓主前面所說的優化大概還能算得上一些基本常識。
            后面越說越糊涂了,哪能用編譯后的匯編來說明"C++"的優化?要知道C++代碼被編譯成什么樣的匯編,是編譯器決定的,這意味著你所謂的"優化"沒有任何意義,或是離開你所用的那個牌子、那個版本的編譯后沒有任何意義,所以談不上優化。  回復  更多評論
              

            # re: C++ 代碼優化 2007-04-06 10:23 jaogoy
            的確,就像for和while比較:

            4: for(; i<2; ++i);
            0040102F jmp main+2Ah (0040103a)
            00401031 mov eax,dword ptr [ebp-4]
            00401034 add eax,1
            00401037 mov dword ptr [ebp-4],eax
            0040103A cmp dword ptr [ebp-4],2
            0040103E jge main+32h (00401042)
            00401040 jmp main+21h (00401031)
            5: while(i<2)++i;
            00401042 cmp dword ptr [ebp-4],2
            00401046 jge main+43h (00401053)
            00401048 mov ecx,dword ptr [ebp-4]
            0040104B add ecx,1
            0040104E mov dword ptr [ebp-4],ecx
            00401051 jmp main+32h (00401042)

            基本沒區別,不能拿些不對稱的來說明問題  回復  更多評論
              

            亚洲va中文字幕无码久久| 成人国内精品久久久久影院VR | 夜夜亚洲天天久久| 精产国品久久一二三产区区别| 精品一久久香蕉国产线看播放| 69久久夜色精品国产69| 亚洲国产精品无码久久久秋霞2| 国产精品久久新婚兰兰| 亚洲国产精品成人AV无码久久综合影院 | 97精品国产97久久久久久免费| 久久精品免费观看| 国产婷婷成人久久Av免费高清| 东京热TOKYO综合久久精品| 久久A级毛片免费观看| 精品国产福利久久久| 中文精品久久久久国产网址| 99久久夜色精品国产网站| 国产精品无码久久久久| 久久久精品波多野结衣| 伊人久久精品影院| 色婷婷综合久久久久中文| 国产精品久久毛片完整版| 久久国产精品二国产精品| 国产精品久久久久蜜芽| 久久国产热精品波多野结衣AV| 2020最新久久久视精品爱 | 久久91精品国产91久久麻豆| 国产精品成人无码久久久久久 | 久久综合狠狠综合久久97色| 久久久精品人妻一区二区三区蜜桃| 亚洲国产精品无码久久一线| 久久精品国产亚洲沈樵| 久久综合久久综合亚洲| 99久久久精品免费观看国产| 精品国产综合区久久久久久| 亚洲AV成人无码久久精品老人| 99久久精品国产一区二区三区| 亚洲伊人久久精品影院| 久久精品国产WWW456C0M| 久久精品日日躁夜夜躁欧美| 国产成人精品综合久久久|