Visual C++ 中的命名返回值優化

多年來,Microsoft Visual C++編譯器一直在努力尋求更新的技術與優化方式,以求最大可能地提高程序的性能。此文描述了Visual C++編譯器在不同情況下,是怎樣消除多餘的複製構造函數和析構函數的。

   通常來說,當方法返回對象的一個實例時,會創建一個臨時對象,並通過複製構造函數複製到目標對象中。在C++標準中,允許省略複製構造函數(哪怕會導致不同的程序行爲),但這有一個副作用,就是編譯器可能會把兩個對象當成一個。Visual C++ 8.0(Visual C++ 2005)充分利用了C++標準的可伸縮性,加入了一些新的特性——命名返回值優化(NRVO)。NRVO消除了基於堆棧返回值的複製構造函數和析構函數,並去除了對多餘複製構造函數和析構函數的調用,從而全面地提高了程序的性能。但要注意到,優化和未優化的代碼,可能會有不同的程序行爲表現。

  而在某些情況下,NRVO不會進行優化(參見優化的侷限性一節),以下是一些常見的情況:

  ·不同的路徑返回不同的命名對象

  ·引入EH狀態的多重返回路徑(甚至所有的路徑中返回相同的命名對象)

  ·由內聯彙編語句引用的命名返回對象

  NRVO優化概述

  以下是一個簡單的示例,演示了優化是怎樣被實現的:

A MyMethod (B &var)
{
 A retVal;
 retVal.member = var.value + bar(var);
 return retVal;
}

  使用上述函數的程序可能會有一個像如下所示的構造函數:

valA = MyMethod(valB);

  由MyMethod返回的值會創建在內存空間中,並通過隱藏的參數指向ValA。以下是函數中帶有隱藏參數,並清晰地寫明構造和析構函數時樣子:

A MyMethod (A &_hiddenArg, B &var)
{
 A retVal;
 retVal.A::A(); // retVal的構造函數
 retVal.member = var.value + bar(var);
 _hiddenArg.A::A(retVal); // A的複製構造函數
 return;
 retVal.A::~A(); // retVal的析構函數
}

  從以上代碼中,很明顯可看出有一些可以優化的地方。最基本的想法是消除基於堆棧的臨時值(retVal),並使用隱藏參數,從而消除那些基於堆棧值的複製構造函數和析構函數。以下是NRVO優化過的代碼:

A MyMethod(A &_hiddenArg, B &var)
{
 _hiddenArg.A::A();
 _hiddenArg.member = var.value + bar(var);
 Return
}

  示例代碼

  Sample1.cpp:比較簡單的示例代碼

#include <stdio.h>
class RVO
{
 public:
  RVO(){printf("I am in constructor\n");}
  RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
  ~RVO(){printf ("I am in destructor\n");}
 int mem_var;
};

RVO MyMethod (int i)
{
 RVO rvo;
 rvo.mem_var = i;
 return (rvo);
}

int main()
{
 RVO rvo;
 rvo=MyMethod(5);
}

  打開或關閉NRVO編譯sample1.cpp,將會產生不同的程序行爲。

  不帶NRVO編譯(cl /Od sample1.cpp),下面是輸出內容:

  I am in constructor
  I am in constructor
  I am in copy constructor
  I am in destructor
  I am in destructor
  I am in destructor

  用NRVO選項編譯(cl /O2 sample1.cpp),以下是輸出內容:

  I am in constructor
  I am in constructor
  I am in destructor
  I am in destructor

  Sample2.cpp:較複雜一點的代碼

#include <stdio.h>
class A {
 public:
  A() {printf ("A: I am in constructor\n");i = 1;}
  ~A() { printf ("A: I am in destructor\n"); i = 0;}
  A(const A& a) {printf ("A: I am in copy constructor\n"); i = a.i;}
  int i, x, w;
};

class B {
 public:
  A a;
  B() { printf ("B: I am in constructor\n");}
  ~B() { printf ("B: I am in destructor\n");}
  B(const B& b) { printf ("B: I am in copy constructor\n");}
};

A MyMethod()
{
 B* b = new B();
 A a = b->a;
 delete b;
 return (a);
}

int main()
{
 A a;
 a = MyMethod();
}

  不帶NRVO(cl /Od sample2.cpp)的輸出如下:

A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in copy constructor
A: I am in destructor
A: I am in destructor
A: I am in destructor

  當打開NRVO優化時,輸出如下:

A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in destructor
A: I am in destructor
  優化的侷限性

  在某些情況下,NRVO優化不會起作用,以下是存在優化侷限性的一些示例程序。

  Sample3.cpp:含有例外的代碼

  在例外(Exception)情況中,隱藏參數必須在它被替換的臨時範圍內析構。

//RVO類定義在sample1.cpp中

#include <stdio.h>

RVO MyMethod (int i)
{
 RVO rvo;
 rvo.mem_var = i;
 throw "I am throwing an exception!";
 return (rvo);
}

int main()
{
 RVO rvo;
 try
 {
  rvo=MyMethod(5);
 }
 catch (char* str)
 {
  printf ("I caught the exception\n");
 }
}

  不帶NRVO編譯(cl /Od /EHsc sample3.cpp),輸出如下:

I am in constructor
I am in constructor
I am in destructor
I caught the exception
I am in destructor

  如果註釋掉“throw”語句,輸出將會如下如示:

I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor

  現在,如果註釋掉“throw”語句,並且用NRVO編譯,程序輸出如下:

I am in constructor
I am in constructor
I am in destructor
I am in destructor

  這就是說,不管打開或關閉NRVO選項,sample3.cpp的程序行爲都一樣。

  Sample4.cpp:不同的命名對象

  想要充分利用優化,所有的返回路徑必須都返回相同的命名對象,請看如下示例代碼:

#include <stdio.h>

class RVO
{
 public:
  RVO(){printf("I am in constructor\n");}
  RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
 int mem_var;
};

RVO MyMethod (int i)
{
 RVO rvo;
 rvo.mem_var = i;
 if (rvo.mem_var == 10)
  return (RVO());
 return (rvo);
}

int main()
{
 RVO rvo;
 rvo=MyMethod(5);
}

  優化打開時(cl /O2 sample4.cpp)的輸出,與沒有進行任何優化時(cl /Od sample.cpp)的輸出是一樣的。因爲不是所有的返回路徑都返回同一命名對象,所以NRVO此時不起任何作用。

I am in constructor
I am in constructor
I am in copy constructor

  如果把上述代碼的所有返回路徑都改爲返回rvo(如下例Sample4_modified.cpp),此時優化就會消除多餘的複製構造函數。

  經過修改的Sample4_Modified.cpp,以利用NRVO。

#include <stdio.h>

class RVO
{
 public:
  RVO(){printf("I am in constructor\n");}
  RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
 int mem_var;
};

RVO MyMethod (int i)
{
 RVO rvo;
 if (i==10)
  return (rvo);
 rvo.mem_var = i;
 return (rvo);
}

int main()
{
 RVO rvo;
 rvo=MyMethod(5);
}

  此時(cl /O2 Sample4_Modified.cpp)的輸出:

I am in constructor
I am in constructor

  Sample5.cpp:EH限制

  以下的Sample5與Sample4基本一致,除了增加了RVO類的析構函數,並具有多重返回路徑,且引入的析構函數在函數中創建了一個EH狀態。由於編譯器跟蹤的複雜性,此類對象通常需要被析構,但它阻止了返回值優化,這也是Visual C++ 2005在將來需要改進的地方。

//RVO類定義在sample1.cpp中

#include <stdio.h>
RVO MyMethod (int i)
{
 RVO rvo;
 if (i==10)
  return (rvo);
 rvo.mem_var = i;
 return (rvo);
}

int main()
{
 RVO rvo;
 rvo=MyMethod(5);
}

  不論打開或關閉優化,Sample5.cpp都會產生相同的結果。

I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor

  要想打開NRVO優化,必須消除多重返回點,可以像如下所示修改MyMethod:

RVO MyMethod (int i)
{
 RVO rvo;
 if (i!=10)
  rvo.mem_var = i;
 return(rvo);
}

  Sample6.cpp:內聯彙編限制

  當命名返回對象被內聯彙編語句所引用時,編譯器不會進行NRVO優化,請看下例代碼:

#include <stdio.h>
// RVO類定義在sample1.cpp中

RVO MyMethod (int i)
{
 RVO rvo;
 __asm {
  mov eax,rvo //可以註釋掉此行
  mov rvo,eax //可以註釋掉此行
 }
 return (rvo);
}

int main()
{
 RVO rvo;
 rvo=MyMethod(5);
}

  即使打開優化選項(cl /O2 sample6.cpp)來編譯sample6.cpp,NRVO也不會起作用。這是因爲內聯彙編語句中引用了返回對象,因此,打開或關閉優化選項,輸出都會像如下所示:

I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor

  從以上輸出,可清楚地看到,複製構造函數和析構函數並沒有被消除。但如果註釋掉彙編語句,優化將會消除掉這些函數調用。
 優化的副作用

  程序員必須意識到,如此之類的優化將會影響到程序的流程,以下的示例代碼演示了優化所帶來的影響。

  Sample7.cpp
 
#include <stdio.h>

int NumConsCalls=0;

int NumCpyConsCalls=0;

class RVO

{

public:

RVO(){NumConsCalls++;}

RVO (const RVO& c_RVO) {NumCpyConsCalls++;}

};

RVO MyMethod ()

{

RVO rvo;

return (rvo);

}

void main()

{

RVO rvo;

rvo=MyMethod();

int Division = NumConsCalls / NumCpyConsCalls;

printf ("Constructor calls / Copy constructor calls = %d\n",Division);

}

  不帶優化選項編譯sample7.cpp(cl /Od sample7.cpp),程序的輸出是在意料之中的,構造函數被調用了兩次,而複製構造函數被調用了一次,因此,輸出的結果爲2。

Constructor calls / Copy constructor calls = 2

  另一方面,如果打開優化選項編譯上述代碼(cl /O2 sample7.cpp),NRVO將會起作用,並消除掉複製構造函數,因此NumCpyConsCalls結果爲零,從而引發程序的除零錯誤。如果像sample7.cpp中那樣沒有很好地處理這種例外錯誤,將會導致程序崩潰。

  從以上可看出,命名返回值優化(NRVO)消除了多餘的函數調用,從而在一定程度上提高了程序的速度,需記住的是,優化有時也會有副作用,請謹慎使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章