與臨時對象的斗爭(下)
作者:唐風(fēng)
原載:www.cnblos.com/liyiwen
在上篇里,我們看到了 (N)RVO 和右值引用,下面我們來看看表達式模板。
Expression Template(表達式模板,ET)
如果有“系統(tǒng)地”學(xué)習(xí)過 C++ 的模板編程,那么你應(yīng)該已經(jīng)知道 Expression Template 這個“東西”。在模板圣經(jīng)《C++ templates》的第 18 章專門用了一整章來講這個技巧,(是的,我認為它是一種技巧)。足以見得它比較復(fù)雜,也很重要。
說起 Expression Template 產(chǎn)生,“臨時對象”也是“功臣”之一啊。還是來用例子來說明(你能很容易找到這樣類似的例子,呵,我就是參照著別人寫的):
class MyVec
{
public:
MyVec(){
p = new int[SIZE];
}
MyVec(MyVec const& a_left) {
p = new int[SIZE];
memcpy(p, a_left.p, SIZE * sizeof(int));
}
~MyVec(){delete [] p;}
MyVec& operator=(MyVec const& a_left) {
if (this != &a_left) {
delete [] p;
p = new int[SIZE];
memcpy(p, a_left.p, SIZE * sizeof(int));
}
return *this;
}
int& operator [](size_t a_idx) {
return p[a_idx];
}
int operator [](size_t a_idx)const {
return p[a_idx];
}
MyVec const operator + (MyVec const& a_left) const {
MyVec temp(*this);
temp += a_left;
return temp;
}
MyVec& operator += (MyVec const& a_left) {
for (size_t i = 0; i < SIZE; ++i) {
p[i] += a_left.p[i];
}
return *this;
}
private:
static int const SIZE = 100;
int* p;
};
int main(int argc, char* argv[])
{
MyVec a, b, c;
MyVec d = a + b + c;
return 0;
}
看,我們寫下這么小段代碼:
MyVec d = a + b + c;
這是很常用的數(shù)學(xué)運算吧,而且代碼很直觀。但這個表達式有一個問題,就是產(chǎn)生了“不必要”的臨時對象。因為 a + b 的結(jié)果會成為一個存在一個臨時對象上 temp 上,然后這個 temp 再加上 c ,最后把結(jié)果傳給 d 進行初始化。如果這些向量很長,或是表達式再加幾節(jié),可以想像這些 temp 會多讓人不爽。
而且,如果我們寫成這樣:
MyVec d = a;
d += b;
d += c;
就可以避免產(chǎn)生多余的臨時對象。但這樣寫,如果是不了解“行情”的人看了MyVec d = a + b + c;之后再看這段,是不是會覺得寫這代碼的人欠K?
好吧,你會問,上面不是說右值引用可以解決這樣問題?是的,但在沒有右值引用的“黑暗日子”里,我們就不用過活了?當然要,小學(xué)開始數(shù)學(xué)老師就教我們要一題多解吧,換個思路也有辦法,這個辦法就是ET。
怎么做的呢?a + b + c 會產(chǎn)生臨時變量是因為 C++ 是即時求值的,在看到 a + b,就先算出一個 temp 的Vector對象,然后再向下算。如果能進行延遲求值,看完整個表達式再來計算,那么就可以避免這個temp的產(chǎn)生。
怎么做?
原來的做法中,operator + 直接進行了計算,既然我們不想它“過早”的計算,那么我們就在重新重載一個operator + 運算符,在這個運算中不進行真正的運算,只是生成一個對象,在這個對象中把加法運算符兩邊的操作數(shù)保留下來~然后讓它參與到下一步的計算中去。(好吧,這個對象也是臨時的,但它的代價非常非常小,我們先不理會它)。于是我們寫下面的代碼:
class MyVec;
template <typename L>
class ExpPlus {
L const & lvec;
MyVec const & rvec;
public:
ExpPlus(L const& a_l, MyVec const& a_r):
lvec(a_l), rvec(a_r)
{ }
int operator [] (size_t a_idx) const;
};
// Point 1
template <typename L>
ExpPlus<L> operator + (L const& a_l, MyVec const & a_r) {
return ExpPlus<L>(a_l, a_r);
}
class MyVec
{
public:
MyVec(){
p = new int[SIZE];
}
MyVec(MyVec const& a_r) {
p = new int[SIZE];
memcpy(p, a_r.p, SIZE * sizeof(int));
}
template <typename Exp>
MyVec(Exp const& a_r) {
p = new int[SIZE];
for (size_t i = 0; i < SIZE; ++i) {
p[i] += a_r[i];
}
}
~MyVec(){delete [] p;}
MyVec& operator = (MyVec const& a_r) {
if (this != &a_r) {
delete [] p;
p = new int[SIZE];
memcpy(p, a_r.p, SIZE * sizeof(int));
}
return *this;
}
template <typename Exp>
MyVec& operator = (Exp const & a_r) {
delete [] p;
p = new int[SIZE];
for (size_t i = 0; i < SIZE; ++i) {
p[i] += a_r[i];
}
return *this;
}
int& operator [](size_t a_idx) {
return p[a_idx];
}
int operator [](size_t a_idx)const {
return p[a_idx];
}
private:
static int const SIZE = 100;
int* p;
};
template <typename L>
int ExpPlus<L>::operator [] (size_t a_idx) const {
return lvec[a_idx] + rvec[a_idx];
}
int main(int argc, char* argv[])
{
MyVec a, b, c;
MyVec d = a + b + c;
return 0;
}
比起之前的代碼來說,這段代碼有幾個重要的修改:首先,我們增加了一個模板類 ExpPlus,用它來代表加法計算的“表達式”,但在進行加法時,它本身并不進行真正的計算。對這個類,定義了下標運算符,這個運算符中才進行了真正的加法計算。然后,對于原來的 MyVec,我們重載它的賦值運算符,讓它在賦值的時候通過ExpPlus的下標運算符來獲得計算結(jié)果(也就是,在賦值操作時才真正的進行了計算!)。
上面這段話,對于不了解ET的人來說,也許一時間還不容易明白,我們一步一步來:
在 d = a + b + c 這個式子中,首先遇到 a + b,這時,模板函數(shù) operator + 會被調(diào)用(代碼中注釋了“Point 1 ”),這時只是生成一個臨時的ExpPlus<MyVec>對象(我們叫它 t1 吧),不做計算,只是保留計算的左右操作數(shù)(也就是a和b),接著,t1 + c ,再次調(diào)用同樣的 operator + ,而且也只是生成一個對象(我們叫它 t2 吧),這個對象的類型是 ExpPlus<ExpPlus<MyVec>>,同樣,t2 在這里只是保留了兩邊的操作數(shù)(也就是 t1 和 c)。直到整個表達式“做完”,沒有任何東西進行了計算,所做的事情實際上只是用 ExpPlus 這個模板類把計算式的信息記錄下來了(當然,這些信息就是參與計算的操作數(shù))。
最后,當進行 d = t2 的時候,MyVec 的賦值運算符被調(diào)用(用 t2 作參數(shù))。注意,這個調(diào)用中的語句 p[i] = t2[i],其中 t2[i] 通過重載的下標運算符,展開成 t1[i] + c[i],同理 t1[i] 又再次展開,成為 a[i]+b[i],最終,p[i] = t2[i] 就變成了:p[i] = a[i] + b[i] + c[i])(當然,里面參雜了內(nèi)聯(lián)的效果,這些函數(shù)都是非常容易被內(nèi)聯(lián)的)。就像變“魔術(shù)”一樣,我們通過ExpPlus完成了“延遲計算”,并避免了大型的 MyVec 臨時對象的產(chǎn)生。
這基本上就是 ET 的“原理”了吧。我們來“專門化”一下 ET 的好處:
- To create a domain-specific embedded language (DSEL) in C++
- To support lazy evaluation of C++ expressions (e.g., mathematical expressions), which can be executed much later in the program from the point of their definition.
- To pass an expression — not the result of the expression — as a parameter to a function.
這樣,用 ET 就能兼顧到“直觀”和“效率”了。
ET 中 C++ 中的類庫里已經(jīng)有非常多的應(yīng)用了(包括 boost 中的多個子庫,以及 Blitz++ 等高性能數(shù)學(xué)庫)
總結(jié)
(N)RVO 是編譯器為我們做的優(yōu)化手段,在能進行優(yōu)化的情況下,NRVO 的表現(xiàn)是非常好的,因為它才真正的避免了臨時對象的產(chǎn)生(rvalue reference 和 expression template 中都可能還存在一些小型臨時對象),但 (N)RVO 有很多的限制條件。右值引用(rvalue reference )和 move 語意彌補了 (N)RVO 的不足之處,使得臨時對象的開銷最小化成為可能,但這也是有局限的,比如,嗯,如果一個類本身不動態(tài)地擁有資源……,那 move 就沒有意義了。Expression Template 保持了表達式直觀和效率兩者,很強大,但很顯然它太復(fù)雜,主要是作為類庫的設(shè)計者的武器。另外,它也可能使得使用者要理解一些“新”東西,比如,如果我想存儲表達式的中間值,那么 <ExpPlus<ExpPlus<...<MyVec>...> 一定會讓我很頭大(不過有了 C++0x 的 auto 就好多了,呵呵)。
全文完!