TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

C++でemplace_backを使う際の注意点

STLvectorのemplace_backを使うと要素の追加時にコンストラクタで初期化できる。

しかし、コンストラクタとデストラクタで副作用のある処理をしている場合、注意が必要だ。
以下のようなコードはうまく動作しない。

#include <iostream>
#include <vector>

using namespace std;

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

int main()
{
	vector<A> vec;
	for (size_t i = 0; i < 2; i++) {
		vec.emplace_back(i + 1);
	}
	return 0;
}

createは2回呼ばれるが、deleteが3回以上呼ばれる。(3回目でメモリ例外になる。)

Visual C++ 2015の場合、2回目のemplace_backの際に、1つ目の要素のdeleteが呼ばれる。
処理系によって動作が異なる。

この動作はvectorが容量を拡張する際に、コピーコンストラクタが呼ばれることによる。

コピーコンストラクタを定義して、確認してみる。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(const A& o) {
		cout << "copy " << size << endl;
		size = o.size;
		a = o.a;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

このコードを実行すると、

create 1
copy 14829735431805717965
delete 1
create 2
delete 1

のように表示される。3回目のdeleteでメモリ例外になる。

対策方法

reserveであらかじめ領域を確保することで、コピーコンストラクタが呼ばれることを避けることができる。

int main()
{
	vector<A> vec;
	vec.reserve(2);
	for (size_t i = 0; i < 2; i++) {
		vec.emplace_back(i + 1);
	}
	return 0;
}

このようにすれば、実行結果は、

create 1
create 2
delete 1
delete 2

となり、デストラクタが2回以上呼ばれることはなくなる。

参考:
C++ vector emplace_back calls copy constructor - Stack Overflow

2018/2/25 追記

平岡さんよりムーブコンストラクタを定義すればよいとコメントをいただきました。

vectorは、領域を拡張する際、メモリの再配置を行うため、要素のムーブを行っている。
このときムーブコンストラクタが定義されていないとコピーコンストラクタが呼ばれてしまう。
コピーした後、コピー元の要素のデストラクタが呼ばれる。
上記コード例のように、コピー先と、コピー元でポインタが同じnewしたオブジェクトを指している場合、コピー元のデストラクタで破棄してしまうと、コピー先で参照できなくなる。
このことは、「プログラミング言語C++ 第4版」にも書かれていた。

クラスを設計する際は、オブジェクトがコピーされる可能性とコピーの方法を必ず検討しなければならない。単純な具象型であれば、メンバ単位のコピーが正しいセマンティクスとなることが多い。しかし、Vectorのような高度な具象型では、メンバ単位のコピーは、正しいセマンティクスとはならない。また、抽象型では、メンバ単位のコピーは、まず、ありえない。

ビャーネ・ストラウストラップ. プログラミング言語C++ 第4版 (Kindle の位置No.3641-3644). . Kindle 版.

ムーブコンストラクタを定義したコードは以下のようになる。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(A&& o) : size(o.size), a(o.a) {
		cout << "move " << size << endl;
		o.a = nullptr;
	}
	~A() {
		cout << "delete " << size << endl;
		delete[] a;
	}
private:
	size_t size;
	int* a;
};

このコードを実行すると、結果は、

create 1
move 1
delete 1
create 2
delete 1
delete 2

となり、正常に実行できる。

ポイントは、ムーブコンストラクタで、o.aにnullptrを代入している箇所だ。
nullptrを代入しておくことで、デストラクタでdelete[]を行ってもムーブ元が参照していたaが破棄されることがなくなる。

unique_ptrを使う方法

unique_ptrを使うことでnullptrの代入とdelete[]を自動的に行うことができる。
こちらが平岡さんに示していただいたコードだ。

class A {
public:
	A(const size_t size_) : size(size_), a(new int[size_]) {
		cout << "create " << size << endl;
	}
	A(A&& o) : size(o.size), a(move(o.a)) {
		cout << "move " << size << endl;
	}
	~A() {
		cout << "delete " << size << endl;
	}
private:
	size_t size;
	unique_ptr<int[]> a;
};

こちらの方が、よりC++的なコードだろう。

Windowsでクリップボードをsortしてuniqする

Windowsコマンドラインでテキストの加工を行うとき、クリップボードと連携できると便利である。

コマンドの結果をクリップボードにコピーするには、clip.exeコマンドにパイプで渡せばよい。

dir /b | clip

逆に、クリップボードのテキストを加工したい場合もある。
今までclip.exeでは入力側のパイプにはできないので、ファイルに保存してから加工していたが、PowerShellならできることがわかった。
普段コマンドプロンプトを使っていて、PowerShellはほとんど使っていなかったけど、sortやuniqもPowerShellならできることがわかった。
MINGWを使っているのでunixコマンド相当がなくて困ることはなかったけど、クリップボード連携を考えるとPowerShellで作業するのもよさそうだ。

PowerShellクリップボードをsortしてuniqする
Get-Clipboard | Sort-Object | Get-Unique | Set-Clipboard

これでクリップボード内のテキストに対して、sortとuniqが行える。

コマンドが長くて覚えづらいが、Tab補完が効く場合がある。sortは、sortと入力してTabを押すと「Sort-Object」に変換される。
uniqは、Tabを押すと、MINGWの「uniq.exe」に補完されてしまった。

help uniq

とすると、部分一致でコマンドを探してくれる。
忘れたときは、helpコマンドで探すとよい。

helpを使えるようにするには、管理者としてPowerShellを起動して、「Update-Help」を実行しておく必要がある。

将棋AIの進捗 その11(マルチGPU対応)

GPUが2つになったので、dlshogiをマルチGPUに対応させました。

ニューラルネットワークの計算要求をキューにためてミニバッチで推論を行う仕組みにしていたので、キューをGPUごとに用意して、探索スレッドを一方のキューに対応させて、キューを監視してニューラルネットワークの予測を行うスレッドをそれぞれのGPUごとに用意する仕組みにしました。

はじめ、単にニューラルネットワークの計算用のスレッドを2つにしてそれぞれが、Pythonの処理を呼び出してChainerに推論を行わせればよいと考えていましたが、Pythonの処理はマルチスレッドで呼び出すとメモリ例外が起きてうまくいきませんでした。

調べたところ、昨日の日記に書いた通りC/C++側のプログラムでもGILを制御する必要がありました。
Pythonの処理はGILがロックされている間は同時には1つしか動かせないので、推論中もGILがロックされていると、マルチGPUにしてもパフォーマンスは上がらないことになります。
GILはIO処理中は解放するのがお作法なので、Chainerも当然GPUで計算中はGILを解放しているはずです。
CuPyのソースを調べたところ、ちゃんとnogilブロックで囲われていました。

Python経由でマルチスレッドの処理を行うと、Pythonの処理中は一つのスレッドしか動作できないため多少効率が落ちますが、ほとんどはGPUで計算している時間なので、影響はそれほどでもないと思います。

TensorFlowを使えば、直接C/C++で推論が行えるのでマルチスレッドでもGILに悩まされることはなくなりますが、ChainerのGPUメモリとCPUメモリのやりとりはかなり効率よくおこなわれているので、TensorFlowで同等の性能をだすにはノウハウがいりそうです。
GPU処理のチューニングまわりでがんばりたくないのでChainerを使っていくつもりです。

マルチGPUの効果

マルチGPUで探索した場合と、シングルGPUで探索した場合で比較を行いました。

測定条件
  • シングルGPUは、TitanV 1枚。CPUは255スレッドで探索。
  • マルチGPUは、Titan VとGeForce 1080 Ti。GPUごとにCPUは255スレッドで探索。
  • モデルは10ブロック、192フィルターのResNet
初手開始局面での、プレイアウト数/秒の測定結果
シングルGPU 5591
マルチGPU 8028

マルチGPUにすることで、1.44倍 プレイアウト数/秒が増えました。

マルチGPUの効果があることが確かめられました。
モンテカルロ木探索でプレイアウト数が増えることは棋力向上につながるはずです。

CPUが合計500スレッド動いているのでCPUボトルネックになっている気もするので、チューニングすればもう少し効率を上げられそうです。

なお、デフォルトの設定ではシングルGPUで動くようにしています。
UCT_Threads2に0以上を設定するとマルチGPUで動きます。


自己対局で局面生成するプログラムもマルチGPUに対応させる予定です。

C/C++からPythonをマルチスレッドで使う

C/C++からPythonの処理をマルチスレッドで使うには、C/C++側でもGILの制御が必要になる。

マルチスレッドでGILを取得せずにPythonの処理を呼び出すとメモリ例外などで異常終了する。

Python仮想マシンはスレッドセーフではなくマルチスレッドでは動かせない。
マルチスレッドで動かすには、どれか一つのスレッドしか動かないようにGILと呼ばれるロックを取得する。
GILを取得しているスレッドがGILを解放すると他のスレッドが動作する。

Pythonの内部処理では、時間のかかるIO処理を行う際は、GILを解放するため、その間は他のスレッドが動作できる。
サードパーティーのライブラリでも、IO処理の際にはGILを解放するように実装されている。

C/C++からPythonの処理を呼び出す際は、GILを取得してから呼び出すようにする。
なお、シングルスレッドの場合は、GILを取得しなくても問題は起きない。

マルチスレッドで処理する際は、メインスレッドでGILを初期化しておく。

PyEval_InitThreads();

子スレッドでGILを取得するには、

PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

とする。

子スレッドでPythonの呼び出しを行った後、GILを解放するには、

PyGILState_Release(gstate);

とする。

Python処理から取得したオブジェクトをC/C++側で使用する場合、Pythonのオブジェクトを使用している間は、GILを解放してはいけない。

C/C++プログラムのメインスレッドで関数の取得などを行い、
子スレッドでPythonの処理を呼び出す場合は、メインスレッドでGILを解放しておく必要がある。

取得しているGILを解放するには、

PyThreadState *_save;
_save = PyEval_SaveThread();

とする。

子スレッドの処理が終了した後、

PyEval_RestoreThread(_save);

で、GILを取得する。

参考:
初期化 (initialization)、終了処理 (finalization)、スレッド — Python 3.10.0b2 ドキュメント

C++でメンバ関数をスレッドで実行する

C++で静的メンバ関数をスレッドで実行するには、以下のように記述する。

class A {
public:
	static void f() {};
};
int main()
{
	thread th(A::f);
	th.join();

	return 0;
}

では、メンバ関数をスレッドで実行するにはどうすればよいだろうか。

class A {
public:
	void f() {};
};
int main()
{
	A a;

	thread th(a.f);
	th.join();

	return 0;
}

このように書くとエラーになる。

	thread th(&a.f);

このように書いてもエラーになる。

thisポインタを引数にする方法

マルチスレッドでクラスのメンバ関数を実行する場合(C++11 std::thread) - Programming Serendipity
この記事によると、引数にthisポインタを渡すことで呼び出せるようだ。

	thread th(&A::f, &a);

このような書き方が可能だ。

なんか処理系依存な気がするが、規格書にある方法らしい。
規格書で読むC++11のスレッド
(p.22)

ラムダ式を使う方法

ラムダ式を使えば、もう少し標準的な書き方ができる。

	thread th([&a]() { a.f(); });

こちらの書き方の方がよいと思う。

クラスのメンバ関数内で別のメンバ関数をスレッドで実行するには、

class A {
public:
	void f() {}
	void Run() {
		thread th([this]() { this->f(); });
		th.join();
	}
};

このように書ける。

ChainerでマルチGPUを試す

GPUが2枚になったので、ChainerでマルチGPUによる学習を試してみた。

MNISTサンプルを使って、実行時間を測定した。

測定条件

  • CUDA v9.0
  • cuDNN 7.0.5
  • ChainerのGitHubレポジトリのexamples/mnistを使用
  • 単体の実行時間はtrain_mnist.pyの実行時間
  • マルチGPUの実行時間はtrain_mnist_data_parallel.pyとtrain_mnist_model_parallel.pyの実行時間
  • オプションはデフォルト
  • GPUはTitan VとGeForce 1080 Tiの2枚

測定結果

条件 実行時間(sec) validation/main/accuracy
Titan V 単体 50.2274 0.9814
GeForce 1080 Ti 単体 50.0922 0.9801
マルチGPU(data_parallel) 30.8084 0.9817
マルチGPU(model_parallel) 140.529 0.9799

考察

単体の実行時間は、どちらも変わらない。
Titan Vの方がCUDAコアとメモリ性能が良いが、MNISTサンプルではGPUの能力を使い切っていないためと思われる。
別途、差が出る条件で測ってみたい。

マルチGPUにすると、data_parallelでは実行時間が61%になっている。
テストデータの一致率は低下していないので、マルチGPUによって効率が上がっている。
ソースを見ると、TrainerのParallelUpdaterを使っているので、data_parallelがChainerの標準的なマルチGPU対応方法と言える。

model_parallelでは、実行時間が単体の実行時間の2.8倍になっている。
テストデータの一致率も低下しており、マルチGPUにより効率が悪くなっている。
ソースを見ると各層で同期をとる処理を行っていたんで、それが遅い原因であると思われる。

作成している将棋AIもマルチGPU対応を行いたいと思う。

将棋AIの進捗 その10(Linux対応)

ChainerのMNISTサンプルをUbuntuで動かすとWindowsよりも早いことがわかったので、dlshogiの自己対局Ubuntuで行えるようにした。

AperyのMakefileを参考に、g++でビルドできるようにした。

Windowsで32スレッドで1手800シミュレーションで自己対局を行うと、2.68局面/秒で生成できる。

Ubuntuで同じ条件で実行すると、期待に反して1.03局面/秒でしか生成できなかった。
コンパイルオプションはAperyにbmi2をターゲットにしたときに合わせているがなぜか遅い。
スレッド数を減らして、16スレッドで実行すると、2.08局面/秒になった。
Windowsで16スレッドで実行すると、1.31局面/秒になる。Windowsではスレッド数が多い方が効率がよい。

CPU側のマルチスレッドの効率に問題がありそうだ。
なお、UbuntuのCPUはCore i9 10コアで、WindowsCore i7 4コアなのでUbuntuの方がCPU性能がよい。

Ubuntuを入れているPCをWindowsで起動して実行すると、同じ条件で、3.16局面/秒で生成できた。
スレッドを120まで増やすと、7.63面/秒になった。
Windowsの場合はCPUのコア数が多い方が早くなる。

対局用の実行ファイルで開始局面を探索させてときのプレイアウト数/秒は、

Ubuntu 553
Windows 4438

と、こちらもUbuntuが非常に遅い結果になった。

原因を調べるのに時間がかかりそう・・・

Makefile
CC = g++
CFLAGS = -std=c++11 -Wextra -Ofast -MMD -MP -fopenmp -DNDEBUG -DHAVE_SSE4 -DHAVE_SSE42 -DHAVE_BMI2 -msse4.2 -mbmi2 -DHAVE_AVX2 -mavx2
LDFLAGS = -lpthread -lboost_python3 -lboost_numpy3 -lpython3.6m -flto
INCLUDE = -I../usi -I../cppshogi -I$(PYENV_ROOT)/versions/anaconda3-5.0.1/include/python3.6m
LIB = -L$(PYENV_ROOT)/versions/anaconda3-5.0.1/lib

target = bin/make_hcpe_by_self_play
sources = self_play.cpp
usi_sources =  mate.cpp ZobristHash.cpp
cppshogi_sources = bitboard.cpp book.cpp common.cpp cppshogi.cpp evalList.cpp evaluate.cpp generateMoves.cpp hand.cpp init.cpp move.cpp movePicker.cpp mt64bit.cpp pieceScore.cpp position.cpp search.cpp square.cpp thread.cpp timeManager.cpp tt.cpp usi.cpp
objects = $(addprefix obj/, $(sources:.cpp=.o))
usi_objects = $(addprefix obj/, $(usi_sources:.cpp=.o))
cppshogi_objects = $(addprefix obj/, $(cppshogi_sources:.cpp=.o))

$(target): $(objects) $(usi_objects) $(cppshogi_objects)
	@[ -d bin ] || mkdir -p bin
	$(CC) -o $@ $^ $(LIB) $(LDFLAGS) $(CFLAGS)

obj/%.o: %.cpp
	@[ -d obj ] || mkdir -p obj
	$(CC) $(CFLAGS) $(INCLUDE) -o $@ -c $<

obj/%.o: ../usi/%.cpp
	@[ -d obj ] || mkdir -p obj
	$(CC) $(CFLAGS) $(INCLUDE) -o $@ -c $<

obj/%.o: ../cppshogi/%.cpp
	@[ -d obj ] || mkdir -p obj
	$(CC) $(CFLAGS) $(INCLUDE) -o $@ -c $<

all: $(target)

clean:
	rm -f $(objects) $(usi_objects) $(cppshogi_objects) $(target)