HEXA BLOG

プログラム

HEXA BLOGプログラム2014.2.26

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

皆さん、こんにちは

 

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

 

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

 

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

 

 

 

 

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

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

 

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

 

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

 

 

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

 

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

 

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

 

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

 

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

 

 

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

 

  1. class MyClass {
  2. public:
  3. typedef std::array<float, 4*1024*1024> buffer_type;
  4.  
  5. MyClass(float f) : _pData(nullptr) {
  6. _pData = new buffer_type;
  7. std::fill( _pData->begin(), _pData->end(), f );
  8. }
  9.  
  10. ~MyClass() {
  11. if( _pData ) {
  12. delete _pData;
  13. }
  14. }
  15.  
  16. // コピーコンストラクタ
  17. MyClass( MyClass& a ) : _pData(nullptr) {
  18. _pData = new buffer_type;
  19. *this = a; // コピー代入演算を呼び出します
  20. }
  21.  
  22. // コピー代入演算
  23. MyClass& operator=( MyClass& a ) {
  24. std::copy( a._pData->begin(), a._pData->end(), _pData->begin() );
  25. return *this;
  26. }
  27.  
  28. // ムーブコンストラクタ
  29. MyClass( MyClass&& a ) : _pData(nullptr) { // 引数は右辺値参照(&&)と呼ばれる型になります
  30. *this = std::move(a); // ムーブ演算代入を呼び出します
  31. }
  32.  
  33. // ムーブ代入演算
  34. MyClass& operator=( MyClass&& a ) {
  35. if( _pData ) {
  36. delete _pData; // 元々、所有していたバッファは解放させます
  37. }
  38. _pData = a._pData; // ムーブ元のバッファをそのまま再利用させます
  39. a._pData = nullptr; // 元のバッファのポインタを無効化(二重解放防止)
  40. return *this;
  41. }
  42.  
  43. private:
  44. buffer_type* _pData;
  45. };

 

 

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

 

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

 

  1. MyClass a(1.0f), b(1.0f);
  2. a = b; // コピー代入演算が呼び出される
  3. MyClass c(a); // コピーコンストラクタが呼び出される
  4. c = std::move(a); // ムーブ代入演算が呼び出される
  5. MyClass d(std::move(c)); // ムーブコンストラクタが呼び出される
  6.  
  7. auto fill2 = []() -> MyClass { return MyClass(2.0f); }
  8. a = fill2(); // ムーブ代入演算が呼び出される

 

 

 

 

 

 

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

 

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

 

  1. template<typename T>
  2. void copySwap(T& a, T& b) {
  3. T tmp = a;
  4. a = b;
  5. b = tmp;
  6. }
  7.  
  8. template<typename T>
  9. void moveSwap(T&a, T& b) {
  10. T tmp = std::move(a);
  11. a = std::move(b);
  12. b = std::move(tmp);
  13. }
  14.  
  15. int main(int argc, char** argv)
  16. {
  17. MyClass src(0.0f), dst(1.0f);
  18.  
  19. std::cout << "time(ms) copySwap : " << time( [&src, &dst]() {
  20. for(size_t i=0; i<1024*64; ++i) {
  21. copySwap( src, dst );
  22. }
  23. }) << std::endl;
  24.  
  25. std::cout << "time(ms) moveSwap : " << time( [&src, &dst]() {
  26. for(size_t i=0; i<1024*64; ++i) {
  27. moveSwap( src, dst );
  28. }
  29. }) << std::endl;
  30.  
  31. return 0;
  32. }

 

【実行結果】

  1. time(ms) copySwap : 1026614
  2. time(ms) moveSwap : 17

 

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

 

 

  1. // 省略
  2. 00D9332A mov dword ptr fs:[00000000h],eax
  3. T tmp = a;
  4. 00D93330 push 1
  5. 00D93332 lea ecx,[tmp]
  6. 00D93335 call MyClass::__autoclassinit (0D913D4h)
  7. 00D9333A mov eax,dword ptr [a]
  8. 00D9333D push eax
  9. 00D9333E lea ecx,[tmp]
  10. 00D93341 call MyClass::MyClass (0D91636h)
  11. 00D93346 mov dword ptr [ebp-4],0
  12. a = b;
  13. 00D9334D mov eax,dword ptr [b]
  14. a = b;
  15. 00D93350 push eax
  16. 00D93351 mov ecx,dword ptr [a]
  17. 00D93354 call MyClass::operator= (0D91613h)
  18. b = tmp;
  19. 00D93359 lea eax,[tmp]
  20. 00D9335C push eax
  21. 00D9335D mov ecx,dword ptr [b]
  22. 00D93360 call MyClass::operator= (0D91613h)
  23. // 省略
  24. 00D9746A mov dword ptr fs:[00000000h],eax
  25. T tmp = std::move(a);
  26. 00D97470 push 1
  27. 00D97472 lea ecx,[tmp]
  28. 00D97475 call MyClass::__autoclassinit (0D913D4h)
  29. 00D9747A mov eax,dword ptr [a]
  30. 00D9747D push eax
  31. 00D9747E call std::move (0D91037h)
  32. 00D97483 add esp,4
  33. 00D97486 push eax
  34. 00D97487 lea ecx,[tmp]
  35. 00D9748A call MyClass::MyClass (0D9150Fh)
  36. 00D9748F mov dword ptr [ebp-4],0
  37. a = std::move(b);
  38. 00D97496 mov eax,dword ptr [b]
  39. 00D97499 push eax
  40. 00D9749A call std::move (0D91037h)
  41. 00D9749F add esp,4
  42. 00D974A2 push eax
  43. 00D974A3 mov ecx,dword ptr [a]
  44. 00D974A6 call MyClass::operator= (0D9137Ah)
  45. b = std::move(tmp);
  46. 00D974AB lea eax,[tmp]
  47. 00D974AE push eax
  48. 00D974AF call std::move (0D91037h)
  49. 00D974B4 add esp,4
  50. 00D974B7 push eax
  51. 00D974B8 mov ecx,dword ptr [b]
  52. 00D974BB call MyClass::operator= (0D9137Ah)
  53. // 省略

 

 

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

 

 

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

 

 

それでは

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

 

RECRUIT

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

RECRUIT SITE 

S