與臨時(shí)對(duì)象的斗爭(上)
作者:唐風(fēng)
原載:www.cnblogs.com/liyiwen
C++ 是一門以效率見長的語言(雖然近來越來越多的人“不齒”談及效率,我深以為不然,在某一次的程序編寫中不對(duì)效率錙銖必較并不意味意味著我們就不應(yīng)該追求更多的更好的做法)。總之吧,相比起其它語言,程序員們?cè)谑?C++ 的時(shí)候會(huì)更加有意識(shí)地去避免沒有效率的做法。在C++ 的程序中,臨時(shí)對(duì)象的產(chǎn)生就是損及效率的“惡因”之一,因此也產(chǎn)生出一些意思的技術(shù)和優(yōu)化手段,這篇文章里我總結(jié)一下最近在這些方面學(xué)習(xí)的一些收獲:
返回值優(yōu)化(RVO)與具命返回值優(yōu)化(NRVO)
這是一項(xiàng)編譯器做的優(yōu)化,已經(jīng)是一種很常見的優(yōu)化手段了,放狗搜一下可以找到很多的資料,在 MSDN 里也有相關(guān)的說明。
返回值優(yōu)化,顧名思義,就是與返回值有關(guān)的優(yōu)化(廢話……),是當(dāng)函數(shù)是按值返回(而不是引用啊、指針?。r(shí),為了避免產(chǎn)生不必要的臨時(shí)對(duì)象以及值拷貝而進(jìn)行的優(yōu)化。
先看看下面的代碼:
typedef unsigned int UINT32;
class MyCla
{
public:
MyCla(UINT32 a_size = 10):size(a_size) {
p = new UINT32[size];
}
MyCla(MyCla const & a_right):size(a_right.size) {
p = new UINT32[size];
memcpy(p, a_right.p, size*sizeof(UINT32));
}
MyCla const& operator = (MyCla const & a_right) {
size = a_right.size;
p = new UINT32[size];
memcpy(p, a_right.p, size*sizeof(UINT32));
return *this;
}
~MyCla() {
delete [] p;
}
private:
UINT32 *p;
UINT32 size;
};
MyCla TestFun() {
return MyCla();
}
int _tmain(int argc, _TCHAR* argv[])
{
MyCla a = TestFun();
return 0;
}
TestFun() 函數(shù)返回了一個(gè) MyCla 對(duì)象,而且是按值傳遞的。
在沒有任何“優(yōu)化”之前,這段代碼的行為也許是這樣的:return MyCla() 這行代碼中,構(gòu)造了一個(gè) MyCla 類的臨時(shí)的無名對(duì)象(姑且叫它t1),接著把 t1 拷貝到另一塊臨時(shí)對(duì)象 t2(不在棧上),然后函數(shù)保存好 t2 的地址(放在 eax 寄存器中)后返回,TestFun 的棧區(qū)間被“撤消”(這時(shí) t1 也就“沒有”了,t1 的生存域在 TestFun 中,所以被析構(gòu)了),在 MyCla a = TestFun(); 這一句中,a 利用 t2 的地址,可以找到 t2 進(jìn)行,接著進(jìn)行構(gòu)造。這樣 a 的構(gòu)造過程就完成了。然后再把 t2 也“干掉”。
可以看到,在這個(gè)過程中,t1 和 t2 這兩個(gè)臨時(shí)的對(duì)象的存在實(shí)在是很浪費(fèi)的,占用空間不說,關(guān)鍵是他們都只是為a的構(gòu)造而存在,a構(gòu)造完了之后生命也就終結(jié)了。既然這兩個(gè)臨時(shí)的對(duì)象對(duì)于程序員來說根本就“看不到、摸不著”(匿名對(duì)象嘛,你怎么引用?),于是編譯器干脆在里面做點(diǎn)手腳,不生成它們!怎么做呢?很簡單,編譯器“偷偷地”在我們寫的fun函數(shù)中增加一個(gè)參數(shù) A&,然后把 a 的地址傳進(jìn)去(注意,這個(gè)時(shí)候 a 的內(nèi)存空間已經(jīng)存在了,但對(duì)象還沒有被“構(gòu)造”,也就是構(gòu)造函數(shù)還沒有被調(diào)用),然后在函數(shù)體內(nèi)部,直接用 a 來代替原來的“匿名對(duì)象”,在函數(shù)體內(nèi)部就完成 a 的構(gòu)造。這樣,就省下了兩個(gè)臨時(shí)變量的開銷。這就是所謂的“返回值優(yōu)化”~!在 VC7 里,按值返回匿名對(duì)象時(shí),默認(rèn)都是這么做。
上面說的是“返回值優(yōu)化(RVO)”,還有一種“具名返回值優(yōu)化(NRVO)”,是對(duì)于按值返回“具名對(duì)象”(就是有名字的變量!)時(shí)的優(yōu)化手段,其實(shí)道理是一樣的,但由于返回的值是具名變量,情況會(huì)復(fù)雜很多,所以,能執(zhí)行優(yōu)化的條件更苛刻,在下面三種情況下(來自MSDN),NRVO 將一定不起作用:
- 不同的返回路徑上返回不同名的對(duì)象(比如if XXX 的時(shí)候返回x,else的時(shí)候返回y)
- 引入 EH 狀態(tài)的多個(gè)返回路徑(就算所有的路徑上返回的都是同一個(gè)具名對(duì)象)
- 在內(nèi)聯(lián)asm語句中引用了返回的對(duì)象名。
不過就算 NRVO 不能進(jìn)行,在上面的描述中的 t2 這個(gè)臨時(shí)變量也不會(huì)產(chǎn)生,對(duì)于 VC 的 C++ 編譯器來說,只要你寫的程序是把對(duì)象按值返回的,它會(huì)有兩種做法,來避免 t2 的產(chǎn)生。拿下面這個(gè)程序來說明:
MyCla TestFun2() {
MyCla x(3);
return x;
}
一種做法是像 RVO一樣,把作為表達(dá)式中獲取返回值來進(jìn)行構(gòu)造的變量 a 當(dāng)成一個(gè)引用參數(shù)傳入函數(shù)中,然后在返回語句之前,用要返回的那個(gè)變量來拷貝構(gòu)造 a,然后再把這個(gè)變量析構(gòu),函數(shù)返回原調(diào)用點(diǎn),a 就構(gòu)造好了。
還有一種方式,是在函數(shù)返回的時(shí)候,不析構(gòu) x ,而直接把 x 的地址放到 exa 寄存器中,返回調(diào)到 TestFun2 的調(diào)用點(diǎn)上,這時(shí),a 可以用 exa 中存著的地址來進(jìn)行構(gòu)造,a 構(gòu)造完成之后,再析構(gòu)原來的變量 x !是的,注意到其實(shí)這時(shí),x 的生存域已經(jīng)超出了 TestFun2,但由于這里 x 所在 TestFun2 的棧雖然已經(jīng)無效,但是并沒有誰去擦寫這塊存,所以 x 其實(shí)還是有效的,當(dāng)然,一切都在匯編的層面,對(duì)于 C++ 語言層面來講是透明的。
嗯,(具名)返回值引用大約就是這么多,在網(wǎng)上和 MSDN 上還能查到更多的例子和解釋,對(duì)于在多線程下 (N)RVO 需要注意什么,嗯,我完全沒有多線程的經(jīng)驗(yàn),不敢亂寫誤人子弟……
右值引用與 move 語意
“C++ 中臨時(shí)對(duì)象對(duì)效率產(chǎn)生的影響一直為人所詬病”(網(wǎng)上流傳的說法),NRVO 等手段也只有在一定程度上彌補(bǔ)這個(gè)不足(你知道,在很多情況下無法做優(yōu)化)。在 C++98 確定后的十多年時(shí)間后,“Cpper神圣”們終于給出了另一個(gè)對(duì)付它的法寶——右值引用。
對(duì)于右值引用,目前我所見過的最好的講解是VC開發(fā)團(tuán)隊(duì)blog中發(fā)布的一篇長文(看這里),在CPP blog上飄飄白云的博主進(jìn)行了全文翻譯(譯得很棒),建議細(xì)讀三遍!理解里面每一個(gè)例子~這樣至少你在右值引用的認(rèn)識(shí)上就有了良好的基礎(chǔ)了。(嗯,我只讀了兩遍,下面說的東西有錯(cuò)誤的話請(qǐng)?jiān)彶⒅赋?:) )
簡單的說,在C++中的左值,就是能取地址的表達(dá)式,比如var、++var之類的,右值就不是能取地址的表達(dá)式啦,比如常數(shù) 123、x++、x+y等等。
嗯,我們可以看到,右值常常就代表著臨時(shí)對(duì)象,也就常常意味著“被詬病的浪費(fèi)……”
比如,z = x + y,這里,翻譯得更“低層”一點(diǎn),那么這里將是:
temp = x + y
z = temp
這個(gè)temp是很尷尬的,不用它將無法實(shí)現(xiàn)正確、良好的 operator + 語意,用它就很難避免臨時(shí)對(duì)象產(chǎn)生的不良開銷。
我們回到上面 RVO 中的程序例子:
MyCla TestFun() {
return MyCla();
}
看,這里返回的 MyCla(),正是一個(gè)右值(我們就給它取個(gè)名吧,不然不好稱呼它,嗯,還叫 t1 吧)。在函數(shù)返回后,這個(gè) t1 就被析構(gòu),它做的析構(gòu)動(dòng)作就是把原來申請(qǐng)的內(nèi)存還給系統(tǒng)。想想在這之前,a 在干什么?a 在構(gòu)造的時(shí)候向系統(tǒng)申請(qǐng)了一塊內(nèi)存!一個(gè)申請(qǐng),一個(gè)還回,一來一回多費(fèi)事啊,如果能直接把 t1 擁有的內(nèi)存給 a ,就不省事了嗎?反正 t1 馬上就要掛了。好,右值引用給了我們這種機(jī)會(huì),我們?yōu)?MyCla 實(shí)現(xiàn)一個(gè) move 語意的拷貝構(gòu)造函數(shù)(不知道什么是 move 拷貝構(gòu)造?回頭看上面鏈接的文章三遍!):
MyCla(MyCla && a_right):size(a_right.size) {
p = a_right.p;
a_right.p = NULL;
}
當(dāng)編譯器探知用于構(gòu)造 a 的是一個(gè)右值時(shí),就調(diào)用這個(gè) move 構(gòu)造函數(shù),然后我們?cè)谶@個(gè)函數(shù)里偷梁換柱,把 t1 的資源竊取過來了。這樣,就算不使用 RVO,這個(gè)構(gòu)造的開銷也是非常小的。
那么,對(duì)于像:
MyCla TestFun2() {
MyCla x(3);
return x;
}
這樣的情況呢?是的,這里的 x 是一個(gè)左值,不會(huì)調(diào)用 move 構(gòu)造函數(shù)。可是我們知道這個(gè) x 其實(shí)馬上也要掛了,它的資源不給白不給啊對(duì)不對(duì)?所以,我們就想告訴編譯器,您就把它當(dāng)成個(gè)右值吧,怎么告訴它呢?用 std::move 來實(shí)現(xiàn)這種 move 語意,像下面這樣:
MyCla TestFun2() {
MyCla x(3);
return std::move(x);
}
好啦,這樣又能用上 MyCla 的 move 構(gòu)造函數(shù)啦。
總結(jié)一下,作為右值的臨時(shí)對(duì)象,其實(shí)它的存在就是充當(dāng)一個(gè)傳遞的橋梁,一旦表達(dá)式過了這個(gè)橋,那么這個(gè)臨時(shí)對(duì)象的存在就沒有意義了,也沒有人能再用到它(因?yàn)樗莻€(gè)右值,沒有名字,又不能取地址)。既然如此,一個(gè)無人問津的就要“死”的變量,把它擁的的資源搶過來也不算過份吧……。在 C++0x 之前,我們想這么做,但是沒有手段,雖然編譯器能分清楚左值右值,但我們無法通過程序告訴編譯器,如果這是左值,請(qǐng)用這個(gè)方法,這個(gè)是右值,嘿嘿,那用另一個(gè)方法幫我搶它的資源吧……,。到了 C++0x ,我們有手段了,那就是右值引用,這個(gè)右值引用可以參與函數(shù)的重載,這樣就給了我們機(jī)會(huì),針對(duì)左右值分別提供不同的操作方法(函數(shù))讓編譯器幫我們選擇一個(gè)合適的。
一般來說,可能需要注意右值引用的地方有:
當(dāng)我們寫的類里擁有動(dòng)態(tài)申請(qǐng)的資源時(shí),那么總是應(yīng)該提供一個(gè)move構(gòu)造函數(shù),這將會(huì)帶給很多好處,可以讓這個(gè)類的使用者(一般是我們自己函數(shù),或是SDL等庫)利用它來提升效率。
如果我們寫的函數(shù)需要利用傳入的(含有動(dòng)態(tài)申請(qǐng)資源的)對(duì)象參數(shù)來構(gòu)造新的變量時(shí),我們可以提供右值引用的重載版本,并在構(gòu)造新對(duì)象時(shí)使用std::move來竊取臨時(shí)對(duì)象的資源。
右值引用在泛型編程中也有極為重要的作用(它能實(shí)現(xiàn)完美轉(zhuǎn)發(fā)),但和本文沒多大關(guān)系,就不多說了。
總之,右值引用是 C++0x 中非常耀眼的一個(gè)新的語言特性,VC2010已經(jīng)將其列入支持范圍(GCC 本人幾乎沒用過,沒了解,不敢妄言[注{ThanksTo OwnWaterloo}:gcc新版本也支持了。 gcc4.4.0 的stl已經(jīng)加上對(duì)move的支持了])。
從實(shí)踐的角度講,它能夠完美地解決 C++ 中長久以來為人們所詬病的臨時(shí)對(duì)象的效率問題。從語言本身來講,它健全了 C++ 中引用類型在左值右值方面原先的缺陷,從庫的設(shè)計(jì)者角度講,它給設(shè)計(jì)者又帶來了一把利器。而對(duì)于廣大的庫使用者而言,不動(dòng)一兵一卒便能獲得“免費(fèi)”的效率提升。
牛吧!這個(gè)特性如此重要如此有用,幾乎可以想見在支持右值的編譯器一旦實(shí)用化,就將產(chǎn)生大量的使用右值引用特性代碼和相關(guān)的idioms,也可能會(huì)遇到和這個(gè)相關(guān)的bug,一句話,趁早學(xué)吧,出來混,總是會(huì)碰上的……。
(上篇完,下篇將分析 Expression Template 在消除臨時(shí)變量中的作用,以及對(duì)三種方法進(jìn)行一個(gè)總結(jié))