DXライブラリでスレッドを使ったファイルロード
Cation
2012/1/29に公開されたDXライブラリ3.07で非同期読み込みが公式にサポートされたので、特殊用途以外では以下の記事の内容はあんまし意味ないです...
……また自力で実装した機能が1ヶ月以内に公式サポートだよどうなってんのorz
DXライブラリ環境でマルチスレッド使って、裏でファイルをロードする
というのが今回の記事のテーマ。
NowLoading中にアニメーションとかしたいですよね。
自分で実装しようとした時に上記の具体的なコード例があんまし見つからなかった上に、あってもC言語ベタベタだったり、betterCなC++だったりするしで結構アレだった。
そして取り敢えずロード失敗とかしないのが出来たっぽいので、記事にしてみた。
まず前提として、DXライブラリはマルチスレッドに対応していない。
スレッドを立ててLoadGraphやらLoadSoundMem、DrawGraphやらを実行すると、データぶっ壊れた状態でロードされたり、描画されなかったりで色々と爆発する。
一応公式でマルチスレッドで死に「にくい」状態にする関数は存在するが、まぁ結構な確率でロード失敗とか使えたもんじゃない。
DXライブラリの関数を並列化は無理...なので
逆に言えば、DXライブラリ固有の動作"のみ"が並列になっていなければよい。(…と思う)
実際に試してみて気づいたのが、どうやらファイルをメモリ上にロードする系の機能(FileRead_openやFileRead_readなど)はマルチスレッドで何も問題がない。
※ 間違ってたらとても恥ずかしいのでツッコミください
という事で、メモリ上に読み込みだけを上記関数で行い、そのメモリ上のファイルからハンドルを作成するだけ。
ハンドル作成に要する時間は僅かなので、60FPSを維持するのも簡単だろうと思う。
実際のコード
動くのはこちら(https://github.com/joy1192/joylib)のDXLibs以下。Boost必須。まぁC++使っててゲーム作っているならBoost使っていると思うので(ry
※ 上記の開発環境はVS2010+Boost1.47.0+DXライブラリ
何してるかを簡単に書くと、
スレッド上で
// ファイルを格納するメモリ領域の確保 // 明示的new/delete面倒なのでvector使っとく std::vector<char> buffer( FileSize, 0 ); // 指定ファイルを開く int FileHandle = FileRead_open( "a.bmp" ) ; // ファイルを丸ごと確保済みメモリに読み込む FileRead_read( &buffer[0], buffer.size(), FileHandle ) ; // ファイルを閉じる FileRead_close( FileHandle ) ; // DXライブラリがマルチスレッドに対応してないので、クリティカルな部分を保護 { // このスコープ内は排他制御するようにする boost::mutex::scoped_lock lock(guard); // メモリ上に存在するファイルを使ってハンドルを作成する関数 // (画像の場合は)CreateGraphFromMemを使ってハンドルを作成 int Handle = CreateGraphFromMem( &buffer[0], buffer.size() ) ; // これでハンドル作れたので、このint値を好みな方法でどっかに格納したり、返せばOK } // ハンドル作ったらbufferの中身は要らないので、関数の終わりでvectorのデストラクタによって破棄
みたいな感じのを実装する。そうすると、普通に裏でロードができている。
描画は試してないのでアレだけれども、まぁメインスレッドで描画してファイルのロードだけ後ろでやればスムーズに動くし良いんじゃないですかね!
これ変だろとかバグあんぞとか指摘してくれると非常に喜びます。
move semanticsについて
人/後輩に教える時用の脳内カンペ。
ムーブセマンティクス(move semantics)というのは、コピーではなく、所有権の移動をさせるように動作するような考え方のこと。
例えば、下記のコードで
MyClass a; // aというインスタンスを作成 MyClass b = a; // aの中身の所有権がbに移動(move)
というような動作をするように書く感じ。
C++11でmove semanticsがッ! という言葉面だけ見ると、C++11があたかも全く新しい概念に対応したかのように(少なくとも昔の自分には)見えてしまう。
が、moveという考え方は昔から存在し、C++03でも書くことは可能だった。
例えば、こんな感じ。
struct MyClass{ int* value; MyClass():value( NULL ){} MyClass(int num):value( new int(num) ){} ~MyClass(){ delete value; } void Move(MyClass& temp){ value = temp.value; temp.value = NULL; } }; int main(){ MyClass a(100); // aというインスタンスを作成 MyClass b; // bという中身が空(value==NULL)のインスタンスを作成 b.Move(a); // aの中身の所有権がbに移動(move) }
ああ、まぁそう書けばそうなるけどさ……というレベルの簡単なコードです、が。
この書き方は、意識的に書く必要があって、面倒くさい。
operator =とか、コピーコンストラクタでこういう動作をした方が明らかにスマート。
でも、コピーのつもりが移動(move)されてたなんて事になったら、凄まじいバグの温床になる(有る筈の物が移動してどっか行っちゃうわけなので)。
でも、不必要なコピーをムーブに置き換えられたら、早くなって素敵だよね……という訳で、速度狂のC++erは、
「じゃあ、"今後、明らかに不要"なのを判別できるようにして、それだけを移動の対象にすればよくね? そう、右辺値とか」
と考えた結果、C++11で導入された右辺値参照を引数に取るコンストラクタ/代入演算子で「移動(move)」を別途定義できるようにしたのでありました!
※右辺値とは、凄まじく簡単に語弊を恐れず言えば、通常のコード上では一時的に生成されたコピー"元"の値であり、コピー先の値にコピーが終わった段階で破棄されるもの。関数の返り値として帰ってきた値とか。
という事で、具体的なコードは↓らへん
http://d.hatena.ne.jp/joynote/20110822/1314012953
http://d.hatena.ne.jp/joynote/20110608/1307523140
C++11に対応しているコンパイラでは、標準ライブラリでもあるSTLもmoveに対応しているので、std::vector
つまり、
今までのコードを書き換えなくても早いよ!
前は意識してテクニカルに書かなきゃいけない部分が見た目簡単に書けるよ!
ということでした!
========================
厳密さを欠いている可能性は非常に高いので、ツッコミは待ってます。
Boost.Geometryを使って2Dゲーム当たり判定で楽をしたい
メモ:
参考:https://sites.google.com/site/boostjp/tips/geometry
線同士の交点を求める
namespace bg = boost::geometry; typedef bg::model::d2::point_xy<double> point; bg::model::linestring<point> line1 = boost::assign::list_of<point>(0, 200)(300, 400); bg::model::linestring<point> line2 = boost::assign::list_of<point>(100, 0)(100, 400); std::deque<point> out; bg::intersection( line1, line2, out); // 交点(100, 266.666687):point型がoutにpush_backされる bg::intersection( line1, line2, out); // 同上。2つ目のpointがpush_backされる
追記
動的ゲームで実用するのは難しいかもしれない。
毎フレーム実行するには判定部分が若干重すぎる感がある(交点求めてる付近)。
これでアルゴリズムの動作確認して、それ用に特化したクラスを自分で作る、といった手法はいい感じかもしれない。
追追記
上記の線の交点を求める的な判定部分を手で書いた場合、大体10〜20倍の高速化が実現できた。
やはりリアルタイムの処理に使うのは厳しそう。
暗黙のmoveとNRVO
VS2010(VC10)にて関数で返す値についての扱いをついったーで突っ込まれて実際書いて確かめた時のメモ。
#include <iostream> #include <vector> #include <boost/timer.hpp> struct Test{ std::vector<int> tmp; Test(){ std::cout << "コンストラクタ" << std::endl; } ~Test(){ std::cout << "デストラクタ" << std::endl; } Test(const Test& obj) : tmp( obj.tmp ){ std::cout << "コピーコンストラクタ" << std::endl; } Test(Test&& obj){ std::swap(tmp,obj.tmp); std::cout << "ムーブコンストラクタ" << std::endl; } }; Test get(int n, boost::timer& timer){ Test tmp; for(int i=0; i < n; ++i){ tmp.tmp.push_back(i*2+3*i); } std::cout << timer.elapsed() << std::endl; timer.restart(); return tmp; } int main(){ boost::timer t; auto tmp = get(10000000,t); std::cout << t.elapsed() << std::endl; }
実行結果:
コンストラクタ 0.072 0 デストラクタ
NRVOが聞いてコピーもムーブも起こらずにそのまま置き換えられた。
これが最速だが、ifでreturn分けたりすると最適化が切れる場合が多い。
そうするとどうなるか。てっきり普通にコピーされるのかなーとか思っていた、VC2008脳だった。が。
#include <iostream> #include <vector> #include <boost/timer.hpp> struct Test{ std::vector<int> tmp; Test(){ std::cout << "コンストラクタ" << std::endl; } ~Test(){ std::cout << "デストラクタ" << std::endl; } Test(const Test& obj) : tmp( obj.tmp ){ std::cout << "コピーコンストラクタ" << std::endl; } Test(Test&& obj){ std::swap(tmp,obj.tmp); std::cout << "ムーブコンストラクタ" << std::endl; } }; Test get(int n, boost::timer& timer){ Test tmp; for(int i=0; i < n; ++i){ tmp.tmp.push_back(i*2+3*i); } std::cout << timer.elapsed() << std::endl; timer.restart(); if( n%2 == 0 ){ return tmp; } tmp.tmp.push_back(1192); return tmp; } int main(){ boost::timer t; auto tmp = get(10000000,t); std::cout << t.elapsed() << std::endl; }
実行結果:
コンストラクタ 0.072 ムーブコンストラクタ デストラクタ 0.001 デストラクタ
あ……ハイ。そうですよね、C++0x(11?)では関数内で宣言された変数が返り値になった場合には原則moveされるんでしたねそういえばはい。
という事で暗黙moveされんじゃねーかstd::moveとか書く必要あるのすげー特定場面じゃないですかやだー!!
ということでしたというメモ。
------------------------------
コメント頂いて気づいたけども、「暗黙変換(継承先スマポ=>継承元スマポのような)」の場合は暗黙moveもされないし、もちろんNRVOもかからないので、std::moveを使うのはすげー特定場面でなく、単に特定場面くらいのニュアンスになりそう。
もちろん暗黙変換考える際にはポリモーフィックな感じに使う場面がメインと考えられて、moveされなくても誤差い場合が多いだろうけれども、そうでない場面も全然ありうるので忘れてはいけない場合でした。
range-based for
gcc 4.6.1をビルドしたので動作確認。
#include <iostream> #include <list> #include <memory> struct Task{ int id; Task(int id) : id(id){ std::cout << "Create Task : " << id << std::endl; } ~Task(){ std::cout << "Delete Task : " << id << std::endl; } void update(){ std::cout << "move Task : " << id << std::endl; } }; int main(){ std::list<std::unique_ptr<Task>> task_list; task_list.emplace_back(new Task(1)); task_list.emplace_back(new Task(2)); task_list.emplace_back(new Task(3)); task_list.emplace_back(new Task(4)); task_list.emplace_back(new Task(5)); task_list.emplace_back(new Task(6)); for(auto it = task_list.begin(); it != task_list.end();){ if( (*it)->id%2 == 0 ){ it = task_list.erase(it); continue; } it++; } // range-based for。 // イテレータではなく中身がそのままitに代入して使われる(ぽい) // auto& でほぼ全て大丈夫なんじゃないか疑惑。これはいいものだ。 for(auto& it : task_list ){ it->update(); } }
実行結果:
Create Task : 1 Create Task : 2 Create Task : 3 Create Task : 4 Create Task : 5 Create Task : 6 Delete Task : 2 Delete Task : 4 Delete Task : 6 move Task : 1 move Task : 3 move Task : 5 Delete Task : 1 Delete Task : 3 Delete Task : 5
やっと……やっとC++にforeachがッ……!
ムーブセマンティクス(Move Semantics)はテクニックにすぎない
正確には、C++0xではムーブセマンティクスを実現"しやすく"なっただけで、
ムーブ自体は昔からのテクニックに過ぎない。
という事をやっと理解したっぽい?メモ。
#include <iostream> #include <vector> #include <string> std::vector<std::string> add_bar(std::vector<std::string> lhs) { lhs.push_back("bar"); return lhs; } int main(){ const size_t v_size = 100000; std::vector<std::string> v1; std::vector<std::string> v2; v1.reserve(v_size*2); for(size_t i=0; i < v_size; ++i){ v1.push_back("foo"); } // クソみたいに重いコピーが起こる v1 = add_bar(v1); // moveされてすぐ終わる v1 = add_bar( std::move(v1) ); // この時点でv1には100000個の"foo"と2個の"bar"が入っている。 // moveする。つまり、"ムーブコンストラクタ(or代入演算子)を呼び出す"。 v2 = std::move(v1); // この時点でv1には何も入っていない。 // この時点でv2には100000個の"foo"と2個の"bar"が入っている。 // そしてその処理は、std::vectorの中身を書いた人がポインタのつなぎ替え等で"実装したもの"である。 return 0; }
============================
Move Semanticsが???となる人の突っかかりポイントは(自分がまさにそうだったが)、新しく特別なメモリ上の操作が自動、あるいは新しい構文で行われるんじゃないと思っている所。
実際は単に、移動してよい場合というのを区別できるムーブコンストラクタやムーブ代入演算子が定義されただけで、中の実装は単なるポインタの操作である。(だから配列がポインタ等を用いず生で使ってある場合にはコピー==ムーブ)
実際にはSTLの内部処理とかに使われていて、一般のプログラマが気にする事ではないけれども、どういう事が行われているかを知っているのと知らないのではだいぶ違うよね的な。
gcc 4.5.3を使うまで:健忘録
はじめてgccをビルドったのでメモ。
未来の自分 or gccでC++0x使ってみたいだけの人用。
shに書いたらそのまま通るノリで手順書いてみる。
http://www29.atwiki.jp/akcnv/pages/28.html
ありがたいこのページを参考にする感じ。
環境はUbuntu10.04 EeePC 1000HE。
homeにインストールするのは正道ではない気はするので、そのうち直そう。うん。
user名=joyとする
cd wget http://ftp.gnu.org/gnu/gcc/gcc-4.5.3/gcc-4.5.3.tar.gz wget http://ftp.gnu.org/gnu/gmp/gmp-5.0.2.tar.gz wget http://ftp.gnu.org/gnu/mpfr/mpfr-3.0.1.tar.gz wget http://www.multiprecision.org/mpc/download/mpc-0.8.2.tar.gz tar zxvf gcc-4.5.3.tar.gz tar zxvf gmp-5.0.2.tar.gz tar zxvf mpfr-3.0.1.tar.gz tar zxvf mpc-0.8.2.tar.gz mkdir gcc mv gmp-5.0.2 gcc/gmp mv mpfr-3.0.1 gcc/mpfr mv mpc-0.8.2 gcc/mpc cd gcc cp -r gmp ../gcc-4.5.3/ cp -r mpfr ../gcc-4.5.3/ cp -r mpc ../gcc-4.5.3/ ../gcc-4.5.3/configure --prefix=$HOME/usr --enable-bootstrap --enable-shared \ --enable-static --enable-shared-libgcc --enable-_cxa_atexit --with-dwarf2 \ --disable-sjlj-exceptions --enable-languages=c,c++ sudo make sudo make install cd echo "PATH=/home/joy/usr/bin:$PATH" >> .bashrc rm -f gcc-4.5.3.tar.gz rm -f gmp-5.0.2.tar.gz rm -f mpfr-3.0.1.tar.gz rm -f mpc-0.8.2.tar.gz sudo rm -rf gcc-4.5.3 sudo rm -rf gcc sudo rm -rf gmp sudo rm -rf mpfr sudo rm -rf mpc
たぶんコレで再起動したらいいハズ。いいPC使ってればmake -j4とかが早そう。
無駄な箇所とかありそうだけど、気力尽きたので調べるのはまた今度。
メインPCのVM上でこれで通るか試してみよう。
もちろんご利用は計画的に。
これをsh化して実行して、たとえOS吹っ飛んでも自己責任で。一応。
- ------------------------
追記:
アップデートして最新(2011/05/21現在)にしないと怒られたが、最新にしたら通った
- ------------------------
追記[2011/06/05]:
CentOSでやったら途中で怒られた。
sudoコマンドがデフォで存在しない and rootじゃないとデフォでsudo使えないってさ。
よって、「yum install sudo」 と、ルートになってから「visudo」で「# User privilege specificationの所に自分のユーザー名追加で無事ビルドできた