HEXA BLOG

プログラム

HEXA BLOGプログラム2014.2.26

ムーブセマンティクスってどうなるの?

皆さん、こんにちは

 

本日でヘキサドライブは7周年になります!!

 

私自身は、昨年4月に入社してから初めて迎える創業記念日です。

 

ヘキサドライブの一員になってから一年経とうとしているんだなぁと
しみじみ感じている、ビッシーです

 

 

 

 

話は逸れてしまいますが、本日の技術ネタということで、 

C++11から導入されたムーブセマンティクスをご紹介します
「ムーブセマンティクス」とは「データの受け渡し」表すための新しい意味表現の事です。

 

例えばswap処理を古い方法で書くと、下記のようになります。

 

template<typename T>
swap(T& a, T& b) {
    T tmp = a;	// 一時データのコピー
    a = b;	// aにbのデータをコピー
    b = tmp;	// tmpにaのデータをコピー
}

 

 

swapするたびにtmpインスタンスを生成しなけばならないため、
型Tが大きいオブジェクトを所有している場合、オーバーヘッドが非常に大きくなってしまいます
(もちろん、型Tの代入が2回行われていることもオーバーヘッドの原因になります)

 

しかし、tmpはあくまでswap用にデータを退避させているだけです。
a,bに対する代入も「データのコピー」が目的ではなく、あくまで「データの受け渡し」をしているだけです。

 

実は代入には、代入元への副作用によって、
「コピー(複製)」「ムーブ(受け渡し)」の二種類が存在していたのです!!

 

C++11以前では、このコピームーブという二つの意味を切り分けて表現することが難しい状態でした

 

そのため、C++11からは左辺値(再参照する値)右辺値(一時的な値)への参照を
型として扱うことができるようになり、
クラスに「ムーブコンストラクタ」「ムーブ代入」実装するだけで、
「データのコピー」「データのムーブ」を明示的に切り分けて処理することができるようになりました

 

 

対応したクラスはこんな感じになります
(分かりやすくするため、バッファデータの管理は生ポインタでやっています)

 

class MyClass {
public:
    typedef std::array<float, 4*1024*1024> buffer_type;

    MyClass(float f) : _pData(nullptr) {
        _pData = new buffer_type;
        std::fill( _pData->begin(), _pData->end(), f );
    }

    ~MyClass() {
        if( _pData ) {
            delete _pData;
        }
    }

    // コピーコンストラクタ
    MyClass( MyClass& a ) : _pData(nullptr) {
        _pData = new buffer_type;
        *this = a;			// コピー代入演算を呼び出します
    }

    // コピー代入演算
    MyClass& operator=( MyClass& a ) {
        std::copy( a._pData->begin(), a._pData->end(), _pData->begin() );
        return *this;
    }

    // ムーブコンストラクタ
    MyClass( MyClass&& a ) : _pData(nullptr) {	// 引数は右辺値参照(&&)と呼ばれる型になります
        *this = std::move(a);	// ムーブ演算代入を呼び出します
    }

    // ムーブ代入演算
    MyClass& operator=( MyClass&& a ) {
        if( _pData ) {
            delete _pData;	// 元々、所有していたバッファは解放させます
        }
        _pData = a._pData;	// ムーブ元のバッファをそのまま再利用させます
        a._pData = nullptr;	// 元のバッファのポインタを無効化(二重解放防止)
        return *this;
    }

private:
    buffer_type* _pData;
};

 

 

大切なのは、ムーブコンストラクタムーブ代入演算を定義する事と、
それぞれの実装を引数の右辺値を有効活用して、効率良くすることです。
(右辺値は再度参照されず、そのまま捨てられる性質を活用しましょう!!)

 

コピームーブは呼び出し側の書き方で切り分けされるので、
std::move以外は特に気にしないで大丈夫です

 

MyClass a(1.0f), b(1.0f);
a = b;				// コピー代入演算が呼び出される
MyClass c(a);			// コピーコンストラクタが呼び出される
c = std::move(a);		// ムーブ代入演算が呼び出される
MyClass d(std::move(c));	// ムーブコンストラクタが呼び出される

auto fill2 = []() -> MyClass { return MyClass(2.0f); }
a = fill2();			// ムーブ代入演算が呼び出される

 

 

 

 

 

 

では、実際にcopymoveで呼び出しコードがどう変わるのか見ていきたいと思います。

 

「copy版のswap」「move版のswap」を用意して計測してみました。

 

template<typename T>
void copySwap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

template<typename T>
void moveSwap(T&a, T& b) {
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

int main(int argc, char** argv)
{
    MyClass src(0.0f), dst(1.0f);

    std::cout << "time(ms) copySwap : " << time( [&src, &dst]() {
        for(size_t i=0; i<1024*64; ++i) {
            copySwap( src, dst );
        }
    }) << std::endl;

    std::cout << "time(ms) moveSwap : " << time( [&src, &dst]() {
        for(size_t i=0; i<1024*64; ++i) {
            moveSwap( src, dst );
        }
    }) << std::endl;

    return 0;
}

 

【実行結果】

time(ms) copySwap : 1026614
time(ms) moveSwap : 17

 

実際に、逆アセンブル結果を覗いてみると・・・

 

 

// 省略
00D9332A  mov         dword ptr fs:[00000000h],eax  
	T tmp = a;
00D93330  push        1  
00D93332  lea         ecx,[tmp]  
00D93335  call        MyClass::__autoclassinit (0D913D4h)  
00D9333A  mov         eax,dword ptr [a]  
00D9333D  push        eax  
00D9333E  lea         ecx,[tmp]  
00D93341  call        MyClass::MyClass (0D91636h) 
00D93346  mov         dword ptr [ebp-4],0  
	a = b;
00D9334D  mov         eax,dword ptr [b]  
	a = b;
00D93350  push        eax  
00D93351  mov         ecx,dword ptr [a]  
00D93354  call        MyClass::operator= (0D91613h)
	b = tmp;
00D93359  lea         eax,[tmp]  
00D9335C  push        eax  
00D9335D  mov         ecx,dword ptr [b]  
00D93360  call        MyClass::operator= (0D91613h) 
// 省略
00D9746A  mov         dword ptr fs:[00000000h],eax  
	T tmp = std::move(a);
00D97470  push        1  
00D97472  lea         ecx,[tmp]  
00D97475  call        MyClass::__autoclassinit (0D913D4h)  
00D9747A  mov         eax,dword ptr [a]  
00D9747D  push        eax  
00D9747E  call        std::move (0D91037h)  
00D97483  add         esp,4  
00D97486  push        eax  
00D97487  lea         ecx,[tmp]  
00D9748A  call        MyClass::MyClass (0D9150Fh)
00D9748F  mov         dword ptr [ebp-4],0  
	a = std::move(b);
00D97496  mov         eax,dword ptr [b]  
00D97499  push        eax  
00D9749A  call        std::move (0D91037h)  
00D9749F  add         esp,4  
00D974A2  push        eax  
00D974A3  mov         ecx,dword ptr [a]  
00D974A6  call        MyClass::operator= (0D9137Ah)
	b = std::move(tmp);
00D974AB  lea         eax,[tmp]  
00D974AE  push        eax  
00D974AF  call        std::move (0D91037h)  
00D974B4  add         esp,4  
00D974B7  push        eax  
00D974B8  mov         ecx,dword ptr [b]  
00D974BB  call        MyClass::operator= (0D9137Ah)
// 省略

 

 

確かに、コンストラクタと代入演算のcall先がディスパッチされています

 

 

必要ないコピーやアロケーションを抑えることは、最適化の第一歩ですね!
皆さんもぜひコピームーブの使い分けをしてみてはいかがでしょうか

 

 

それでは

8年目のヘキサドライブもよろしくお願いします!!

 

RECRUIT

大阪・東京共にスタッフを募集しています。
特にキャリア採用のプログラマー・アーティストに興味がある方は下のボタンをクリックしてください

RECRUIT SITE