日別アーカイブ: 2013年2月13日

C++11 Rvalue Reference 右辺値参照

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 が作られているからです。

さらに適当なベクターを作ってみます。

template
class 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;
        MyVector  string_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 自体も複製できるようにしてみます。

template
class 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;
        MyVector  string_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