自入行以來(lái),無(wú)論是查閱資料、技術(shù)博客亦或是同事間的技術(shù)交流,都有一個(gè)共識(shí):在循環(huán)的時(shí)候,務(wù)必使用前置操作符,因?yàn)槠湫阅軆?yōu)于后置操作符,久而久之,這個(gè)就像一個(gè)不成文的規(guī)定,大家都在遵循,久而久之,成為潛移默化的編碼習(xí)慣。而使得大家持有這個(gè)觀點(diǎn)的原因就是后置操作會(huì)產(chǎn)生臨時(shí)變量,而后置操作則不會(huì)。
今天,借助本文,來(lái)分析或者證明下該結(jié)了正確與否~~
原因
后置操作和前置操作,一個(gè)會(huì)產(chǎn)生臨時(shí)變量,一個(gè)不會(huì)產(chǎn)生臨時(shí)變量,其原因是:前置操作遵循的規(guī)則是change-then-use,而后置操作遵循的規(guī)則是use-then-change。正因?yàn)楹笾貌僮鞯膗se-then-change原則,使得編譯器在實(shí)現(xiàn)該操作的時(shí)候,先把之前的值進(jìn)行拷貝備份,然后對(duì)值進(jìn)行更改操作,最后返回之前備份的值。
以整型為例:
?
//?++i i?=?i+1; return?i; //?i++ temp?=?i; i=i+1; return?temp;
?
同樣,對(duì)于復(fù)雜類型:
?
//?++i Object?&operator++()?{????? ??++value_;????? ??return?*this;??? }???? //?i++???? Object?operator++(int)?{????? ??Object?old?=?*this;???? ??????++*this;????? ??return?old;?? }??
?
也正是基于上述原因,我們通常會(huì)得出一個(gè)結(jié)論:前置操作比后置操作更快。
那么,真的是這樣么?下面將分別從內(nèi)置類型和非內(nèi)置類型兩個(gè)方面進(jìn)行分析。
內(nèi)置類型
為了便于分析二者的性能,寫了個(gè)測(cè)試代碼,用來(lái)比較前置++和后置++的性能差異,代碼如下:
pre_inc.cc
?
void?PreInc()?{ ??for?(int?i?=?0;?i?100000;?++i)?{ ?????for?(int?j?=?0;?j?100000;?++j)?{ ???????for?(int?k?=?0;?k?1000;?++k); ?????} ???} } int?main()?{ ??PreInc(); ??return?0; }??
?
post_inc.cc
?
void?PostInc()?{ ??for?(int?i?=?0;?i?100000;?i++)?{ ?????for?(int?j?=?0;?j?100000;?j++)?{ ???????for?(int?k?=?0;?k?1000;?k++); ?????} ???} } int?main()?{ ??PostInc(); ??return?0; }??
?
編譯運(yùn)行之后,比較二者的運(yùn)行時(shí)間,對(duì)比圖如下:
耗時(shí)竟然一樣,顛覆了之前對(duì)這塊的認(rèn)知。
使用下述命令生成匯編代碼(使用-O0禁用優(yōu)化以免結(jié)果產(chǎn)生偏差):
?
$?g++?-O0?-S?pre.cc $?g++?-O0?-S?post.cc
?
查看上述兩個(gè)匯編文件的不同點(diǎn)(使用vimdiff):
通過(guò)上述對(duì)比,發(fā)現(xiàn)前置++和后置++的匯編結(jié)果一致,這也就是說(shuō)至少對(duì)于內(nèi)置類型(上述代碼使用的是int),前置++和后置++的性能一樣。在進(jìn)行搜索的時(shí)候,發(fā)現(xiàn)了下面這段話:
“The compiler will optimize it away” is an?incredibly?lazy justification for using i++ instead of ++i. Moreover, it is basically only true for built-in types, not for class types.
從上述可以看出,對(duì)于內(nèi)置類型的后置++操作,編譯器會(huì)進(jìn)行優(yōu)化,而對(duì)于非內(nèi)置內(nèi)存,則不會(huì)進(jìn)行優(yōu)化,那么到底是不是這樣呢?
自定義類型
迭代器
對(duì)于C++開發(fā)人員,在遍歷vector、list或者set等結(jié)構(gòu)的時(shí)候,都習(xí)慣于使用迭代器即iterator進(jìn)行遍歷,而gcc實(shí)現(xiàn)中,對(duì)iterator(此處只羅列了vector相關(guān))的定義如下:
?
typedef?__gnu_cxx::__normal_iterator?iterator;,>
?
從上述定義可以看出,iterator不是內(nèi)置類型,同內(nèi)置類型一樣,**iterator也支持前置++和后置++**,所以,在本節(jié)中使用迭代器的前置++和后置++對(duì)容器進(jìn)行遍歷,以測(cè)試其性能,代碼如下:
?
#include? #include? #include? #include? int?main(int,?char**) { ??std::vector?v1(?1000000?); ??std::vector?v2(?1000000?); ??std::iota(?v1.begin(),?v1.end(),?1?); ??std::iota(?v2.begin(),?v2.end(),?2?); ??std::time_point?t1,?t2,?t3; ??t1?=?std::now(); ??for(?auto?it?=?v1.begin();?it?!=?v1.end();?++it?) ????*it?*=?2; ??t2?=?std::now(); ??for(?auto?it?=?v2.begin();?it?!=?v2.end();?it++?) ????*it?*=?2; ??t3?=?std::now(); ??std::duration?d1?=?t2?-?t1; ??std::duration?d2?=?t3?-?t2; ??std::cout?<"pre?time?cost:?"?<?
編譯并運(yùn)行:
?
?g++?--std=c++11?test.cc?-o?test;?./test ?pre?time?cost:?44008us ?post?time?cost:??58283us?
通過(guò)上述結(jié)果可以看出,對(duì)于非內(nèi)置類型(或者更確切的說(shuō)對(duì)于迭代器類型),前置操作的性能優(yōu)于后置。
上面從執(zhí)行時(shí)間的角度分析了迭代器的前置操作和后置操作對(duì)性能的影響,下面是STL中對(duì)iterator的源碼:
?
__normal_iterator& ???????operator++()?//?前置操作 ???????{ ?????++_M_current; ?????return?*this; ???????} ?__normal_iterator ???????operator++(int)?//?后置操作 ???????{?return?__normal_iterator(_M_current++);?}?
從上面代碼可以看出,迭代器的前置和后置操作主要有以下兩個(gè)區(qū)別:
??返回值:前置操作返回對(duì)象的引用,后置操作返回類型為對(duì)象,
??拷貝:前置操作無(wú)拷貝操作,后置操作存在一次對(duì)象拷貝
正式因?yàn)檫@兩個(gè)原因,前置操作符就地修改對(duì)象,而后置操作符將導(dǎo)致創(chuàng)建臨時(shí)對(duì)象,調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)(某些情況下編譯器會(huì)做優(yōu)化,此處不做討論),導(dǎo)致了前置操作和后置操作的性能差異。
自定義對(duì)象
在上一節(jié)中,我們通過(guò)迭代器(前置遞增和后置遞增)遍歷對(duì)vector進(jìn)行遍歷,證明了前置遞增的性能優(yōu)于后置遞增,在本節(jié)中,將自定義一個(gè)對(duì)象,然后進(jìn)行測(cè)試。
代碼如下(在最開始的自定義對(duì)象中,只有整數(shù)value_而沒(méi)有v_變量,這就導(dǎo)致測(cè)試結(jié)果很相近,所以為了更加明顯的看出其差異,所以增加了vector):
?
class?Object?{ ??public: ????Object(int?value) ??????:?value_(value)?{ ????????v_.emplace_back(value_); ??????} ????int?Get()?{?return?value_;?} ????Object?&operator++()?//?前置操作 ????{ ??????++value_; ??????v_.emplace_back(value_); ??????return?*this; ????} ????Object?operator++(int)?//?后置操作 ????{ ??????Integer?tmp?=?*this;?//?拷貝,注意此處這個(gè)拷貝行為以及臨時(shí)變量 ??????++value_; ??????v_.emplace_back(value_); ??????return?tmp;?// ????} ??private: ????std::vector?v_; ????int?value_; }; int?main()?{ ??std::time_point?t1,?t2,?t3; ??t1?=?std::now(); ??Integer?i(0); ??for?(int?j?=?0;?j?10000;?++j)?{ ????++i; ??} ??t2?=?std::now(); ??Integer?k(0); ??for?(int?j?=?0;?j?10000;?++j)?{ ????k++; ??} ??t3?=?std::now(); ?? ??std::duration?d1?=?t2?-?t1; ??std::duration?d2?=?t3?-?t2; ??std::cout?<"pre?time?cost:?"?<?
編譯并運(yùn)行:
?
g++?--std=c++11?test.cc?-o?test;?./test pre?time?cost:?188us post?time?cost:??29625us?
從上述測(cè)試結(jié)果可以進(jìn)一步看出,前置++的性能優(yōu)于后置++。
對(duì)于內(nèi)置類型來(lái)說(shuō),前置++和后置++的性能一樣,這是因?yàn)榫幾g器會(huì)對(duì)其進(jìn)行優(yōu)化;而對(duì)于自定義類型的前置和后置操作,你可能會(huì)有疑問(wèn),為什么編譯器不能像優(yōu)化內(nèi)置類型一樣,優(yōu)化自定義類型呢?這是因?yàn)橐蕾囉趫?chǎng)景。在某些場(chǎng)景下編譯器可以進(jìn)行優(yōu)化(主要是拷貝部分),但是在某些情況下,編譯器無(wú)法在不更改代碼含義的情況下對(duì)其進(jìn)行優(yōu)化。所以,除非需要后置操作,否則建議使用前置操作。
結(jié)語(yǔ)
在本文中,分別從內(nèi)置類型的前后置操作和自定義類型的后置操作進(jìn)行性能對(duì)比,結(jié)果發(fā)現(xiàn)對(duì)于內(nèi)置類型,二者在性能上無(wú)差異,而對(duì)于自定義類型,前置操作的性能遠(yuǎn)優(yōu)于后置操作。
除非必須使用i++以滿足編碼場(chǎng)景,否則,在任何情況下都建議使用++i這種前置操作。
很多人都會(huì)認(rèn)為前置操作性能優(yōu)于后置操作,這是錯(cuò)誤的觀點(diǎn),在此進(jìn)行下糾正,準(zhǔn)確的說(shuō)法應(yīng)該是前置操作不會(huì)比后置操作性能差。
審核編輯:湯梓紅
?
,>
評(píng)論