C++11 の新機能を使うとオブジェクトの無駄なコピーを減らすことができます。
記述が簡単になるなど、C++11 にはいろいろ便利に使える追加機能があります。
Rvalue Reference の場合はむしろコードが増えるのですが、
うまく使うことでプログラムの動作を効率化することができます。
とりあえず適当に文字列型を作ってみます。
class MyString { char* Name; private: void Clear() { delete[] Name; Name= NULL; } void Copy( const char* str ) { Clear(); size_t length= strlen(str)+1; Name= new char[length]; memcpy( Name, str, sizeof(char)*length ); } void DeepCopy( const MyString& src ) { Copy( src.Name ); } public: MyString() : Name( NULL ) {} MyString( const MyString& src ) : Name( NULL ) { DeepCopy( src ); } MyString( const char* str ) : Name( NULL ) { Copy( str ); } ~MyString() { Clear(); } MyString& operator=( const MyString& src ) { DeepCopy( src ); return *this; } };
copy 時に文字列バッファの複製が発生します。
例えばこんな感じ。
MyString str1; MyString str2; str1= "abcdefg"; // -- (1) str2= MyString( "ABCDEF" ); // -- (2) str1= str2; // -- (3)
C++11 の Rvalue Reference を使うと特定のケースで copy を move に変換出来ます。
対応コードを追加したのが下記の形。
#include#include #ifndef IS_CPP11 # define IS_CPP11 (__cplusplus > 199711L) #endif int AllocCount; int FreeCount; class MyString { char* Name; private: void Clear() { if( Name ){ FreeCount++; } delete[] Name; Name= NULL; } void Copy( const char* str ) { Clear(); size_t length= strlen(str)+1; Name= new char[length]; memcpy( Name, str, sizeof(char)*length ); AllocCount++; } void DeepCopy( const MyString& src ) { Copy( src.Name ); } public: MyString() : Name( NULL ) {} MyString( const MyString& src ) : Name( NULL ) { DeepCopy( src ); } MyString( const char* str ) : Name( NULL ) { Copy( str ); } ~MyString() { Clear(); } MyString& operator=( const MyString& src ) { DeepCopy( src ); return *this; } //-- ↓ここから追加分 #if IS_CPP11 MyString( MyString&& src ) { Name= src.Name; src.Name= NULL; } MyString& operator=( MyString&& src ) { char* tmp= Name; Name= src.Name; src.Name= tmp; return *this; } #endif };
これで転送元を捨てても構わない場合に copy ではなく破壊転送 (move) が行われます。
上の場合はメモリを確保し直す必要がないので効率が上がります。
転送元を捨てても構わないケースの代表が、
一時確保されたオブジェクトである rvalue 右辺値です。
また明示的に std::move() をつければ任意の値を破壊転送することができます。
これで C++11 の場合は (1),(2) において copy が消えます。
どちらも一時的な object が作られているからです。
さらに適当なベクターを作ってみます。
templateclass MyVector { T* Buffer; T* Ptr; size_t BufferSize; private: void Clear() { delete[] Buffer; Buffer= Ptr= NULL; } public: MyVector() : Buffer( NULL ), Ptr( NULL ), BufferSize( 0 ) {}; MyVector( size_t size ) : BufferSize( size ) { Ptr= Buffer= new T[size]; } ~MyVector() { Clear(); } size_t Size() const { return Ptr - Buffer; } T& operator[]( size_t index ) { return Buffer[index]; } const T& operator[]( size_t index ) const { return Buffer[index]; } void PushBack( const T& src ) { assert( Ptr < Buffer + BufferSize ); *Ptr++= src; } #if IS_CPP11 void PushBack( T&& src ) { assert( Ptr < Buffer + BufferSize ); *Ptr++= std::forward (src); } #endif };
ループさせてどの程度高速化されるか測ってみます。
// main.cpp int main() { AllocCount= 0; FreeCount= 0; { const int LOOP_MAX= 100000; MyVectorstring_list( LOOP_MAX ); for( int i= 0 ; i< LOOP_MAX ; i++ ){ string_list.PushBack( "12345" ); } } printf( "alloc=%d free=%d\n", AllocCount, FreeCount ); return 0; }
差が出るように作ったので C++11 でコンパイルした方が速いのは当然なのですが、
実際にメモリ確保の回数が半減していることがわかります。
Prog time output c03 0m0.015s alloc=200000 free=200000 c11 0m0.009s alloc=100000 free=100000
# Makefile mac: clang++ -std=c++11 -stdlib=libc++ -O4 main.cpp -o c11 clang++ -std=c++03 -stdlib=libc++ -O4 main.cpp -o c03 linux: g++ -std=c++11 -O4 main.cpp -o g11 g++ -std=c++03 -O4 main.cpp -o g03 clang++ -std=c++11 -O3 main.cpp -o c11 clang++ -std=c++03 -O3 main.cpp -o c03 win: cl /O2 main.cpp /Fev11.exe -DIS_CPP11=1 cl /O2 main.cpp /Fev03.exe -DIS_CPP11=0
特定のケースで明らかな無駄を省いているだけなので、通常はここまで差がでません。
例えば (3) の場合何もせずに copy を減らすことはできません。
move の場合は結局 move 用に別の API を設けていることになります。
またオブジェクトだけでなく、copy が発生する API を経由する場合も
copy の他に move 用の別のルートを追加する必要があります。
途中で move() や forward() を付け忘れると、オブジェクトまで到達しないで
途切れてしまいます。
上の MyVector 自体も複製できるようにしてみます。
templateclass MyVector { T* Buffer; T* Ptr; size_t BufferSize; private: void Clear() { delete[] Buffer; Buffer= Ptr= NULL; } #if IS_CPP11 void Move( MyVector&& src ) { Clear(); BufferSize= src.BufferSize; Ptr= Buffer= new T[BufferSize]; for( int i= 0 ; i< BufferSize ; i++ ){ *Ptr++= std::move(src[i]); //-- (4) } } #endif void DeepCopy( const MyVector& src ) { Clear(); BufferSize= src.BufferSize; Ptr= Buffer= new T[BufferSize]; for( int i= 0 ; i< BufferSize ; i++ ){ *Ptr++= src[i]; } } public: MyVector() : Buffer( NULL ), Ptr( NULL ), BufferSize( 0 ) {}; MyVector( size_t size ) : BufferSize( size ) { Ptr= Buffer= new T[size]; } ~MyVector() { Clear(); } size_t Size() const { return Ptr - Buffer; } void PushBack( const T& src ) { assert( Ptr < Buffer + BufferSize ); *Ptr++= src; } #if IS_CPP11 void PushBack( T&& src ) { assert( Ptr < Buffer + BufferSize ); *Ptr++= std::forward (src); } #endif T& operator[]( size_t index ) { return Buffer[index]; } const T& operator[]( size_t index ) const { return Buffer[index]; } MyVector& operator=( const MyVector& src ) { DeepCopy( src ); return *this; } #if IS_CPP11 MyVector& operator=( MyVector&& src ) { Move( std::move(src) ); return *this; } #endif };
下記のように MyVector を代入できるようになりました。
この (5) のケースでは完全な move となり string のメモリ確保が発生しません。
int main() { AllocCount= 0; FreeCount= 0; { const int LOOP_MAX= 100000; MyVectorstring_list( LOOP_MAX ); for( int i= 0 ; i< LOOP_MAX ; i++ ){ string_list.PushBack( "12345" ); } MyVector string_list2( LOOP_MAX ); string_list2= std::move(string_list); // -- (5) } printf( "alloc=%d free=%d\n", AllocCount, FreeCount ); return 0; }
ですが std::move() を付け忘れると move でなく MyString の copy になります。
下記のように alloc/free が増えます。(5) でも同じです。
c03 alloc=300000 free=300000 c11 alloc=200000 free=200000 ( (5) or (6) の move 無し ) c11 alloc=100000 free=100000