HEXA BLOG
ヘキサブログ
プログラム
ムーブセマンティクスってどうなるの?
皆さん、こんにちは
本日でヘキサドライブは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(); // ムーブ代入演算が呼び出される
では、実際にcopyとmoveで呼び出しコードがどう変わるのか見ていきたいと思います。
「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年目のヘキサドライブもよろしくお願いします!!
CATEGORY
- about ヘキサ (166)
- 部活動 (6)
- CG (18)
- プロジェクトマネジメント (1)
- 研修 (5)
- 美学 (1)
- いいモノづくり道 (230)
- 採用 -お役立ち情報も- (149)
- プログラム (188)
- デザイン (99)
- ゲーム (274)
- 日記 (1,104)
- 書籍紹介 (113)
- その他 (875)
- 就活アドバイス (20)
- ラーメン (3)
- ライフハック (25)
- イベント紹介 (10)
- 料理 (23)
- TIPS (7)
- 怖い話 (3)
- サウンド (5)
- 子育て (1)
- 筋トレ (1)
- 商品紹介 (21)
- アプリ紹介 (31)
- ソフトウェア紹介 (33)
- ガジェット紹介 (12)
- サイト紹介 (10)
- 研究・開発 (34)
- 回路図 (4)
- アナログゲーム (40)
- 交流会 (21)
- 報告会 (3)
- インフラ (25)
- グリとブラン (6)
- カメラ (9)
- クラフト (27)
- 部活 (14)
- 画伯 (15)
- カレー (6)
- 音楽(洋楽) (6)
- 映画・舞台鑑賞 (43)
- 飼育 (5)
- いぬ (8)
- ねこ (19)
ARCHIVE
- 2024年
- 2023年
- 2022年
- 2021年
- 2020年
- 2019年
- 2018年
- 2017年
- 2016年
- 2015年
- 2014年
- 2013年
- 2012年
- 2011年
- 2010年
- 2009年
- 2008年
- 2007年