ai65536's blog

将棋とかプログラムとか

32bit版AperyについてApery作者の平岡さんが興味深いツイートをしていました。

ehashというのは評価値のハッシュのことですかね。

では、該当部分のコードを見てみましょう。

// 64bit 変数1つなのは理由があって、
// データを取得している最中に他のスレッドから書き換えられることが無くなるから。
// lockless hash と呼ばれる。
// 128bit とかだったら、64bitずつ2回データを取得しないといけないので、
// key と score が対応しない可能性がある。
// transposition table は正にその問題を抱えているが、
// 静的評価値のように差分評価をする訳ではないので、問題になることは少ない。
// 64bitに収まらない場合や、transposition table なども安全に扱いたいなら、
// lockする、SSEやAVX命令を使う、チェックサムを持たせる、key を複数の変数に分けて保持するなどの方法がある。
// 32bit OS 場合、64bit 変数を 32bitずつ2回データ取得するので、下位32bitを上位32bitでxorして
// データ取得時に壊れていないか確認する。
// 31- 0 keyhigh32
// 63-32 score
struct EvaluateHashEntry {
	u32 key() const     { return static_cast<u32>(word); }
	Score score() const { return static_cast<Score>(static_cast<s64>(word) >> 32); }
	void save(const Key k, const Score s) {
		word = static_cast<u64>(k >> 32) | static_cast<u64>(static_cast<s64>(s) << 32);
	}
#if defined __x86_64__
	void encode() {}
	void decode() {}
#else
	void encode() { word ^= word >> 32; }
	void decode() { word ^= word >> 32; }
#endif
	u64 word;
};
Score evaluate(Position& pos, SearchStack* ss) {
	if (ss->staticEvalRaw != INT_MAX) {
		// null move の次の手の時のみ、ここに入る。
		assert((pos.turn() == Black ? ss->staticEvalRaw : -ss->staticEvalRaw) == evaluateUnUseDiff(pos));
		return (pos.turn() == Black ? ss->staticEvalRaw : -ss->staticEvalRaw) / FVScale;
	}

	const u32 keyHigh32 = static_cast<u32>(pos.getKey() >> 32);
	const Key keyExcludeTurn = pos.getKeyExcludeTurn();
	// ポインタで取得してはいけない。lockless hash なので key と score を同時に取得する。
	EvaluateHashEntry entry = *g_evalTable[keyExcludeTurn];
	entry.decode();
	if (entry.key() == keyHigh32) {
		ss->staticEvalRaw = entry.score();
		assert((pos.turn() == Black ? ss->staticEvalRaw : -ss->staticEvalRaw) == evaluateUnUseDiff(pos));
		return (pos.turn() == Black ? entry.score() : -entry.score()) / FVScale;
	}

	const Score score = static_cast<Score>(evaluateBody(pos, ss));
	entry.save(pos.getKey(), score);
	entry.encode();
	*g_evalTable[keyExcludeTurn] = entry;
	return (pos.turn() == Black ? score : -score) / FVScale;
}

ハッシュから評価値をとってきて、壊れていたら再計算するというようなコードでしょうか。
lock free hash とかlock less hashとか言われている処理ですかね。

詳細はよく分かりませんが、bonanzaにも同じような処理が入っていたと思います。

実験

コードを見ても妥当性がよくわからないので、
おかしいなら複数スレッド側が弱いだろうとの予測から、
1スレッドと複数スレッドの設定で自己対戦してテストしてみました。

設定は
・対局数は10局で先後を入れ替えながら対局
・スレッド数とPonderをOFF以外はデフォルトのままです
・時間は1手2秒

テストに使った32bit版はここに置いています。
https://sites.google.com/site/shogixyz/home/apery


Apery 32bit nosse

スレッド数1 スレッド数2
2勝 8勝

スレッド数1 スレッド数4
2勝 7勝 引き分け1


Apery 64bit sse4.1

スレッド数1 スレッド数2
3勝 7勝

スレッド数1 スレッド数4
1勝 9勝


Aper 32bit nosse (encode, decode部分を空行にした場合)

スレッド数1 スレッド数2
5勝 5勝
スレッド数1 スレッド数4
2勝 8勝

結論

スレッド数を増やすと強くなるの問題ないでしょう。
単純に負けなくなると思っていたのですが、64bit版も勝率100%とはいかないので、
こんなものなんでしょう。

しかし、xorしているコード除去してもそんなに弱くなってない?
という疑問がのこりました。もうちょっと弱くなると思ったんですが。


まあ今回の実験の有効性は少し疑問が疑問残りましたが、32bit版を使っておかしいなとおもったらスレッド数を1にすればいいわけですし、それで性能に不安があるのであればPCを買い替えましょう。