英文:
Chess: quiescence search dominating runtime
问题
我已经为我的国际象棋引擎实现了一个带有静态搜索的Alpha-Beta剪枝算法。然而,在大多数局面中,静态搜索占据了总执行时间的80-90%,这是由我的性能分析器指示的。我在剪枝方面是否有错误?
我包括了Alpha-Beta算法和静态搜索算法的代码。
我的静态搜索直接基于这个伪代码。
// 执行Alpha-Beta搜索。
func ab(b *dragontoothmg.Board, alpha int16, beta int16, depth int8, halt chan bool, stop *bool) (int16, dragontoothmg.Move) {
nodeCount++
if *stop {
return alpha, 0
}
found, tableMove, tableEval, tableDepth, tableNodeType := transtable.Get(b)
if found && tableDepth >= depth {
if tableNodeType == transtable.Exact {
return tableEval, tableMove
} else if tableNodeType == transtable.LowerBound {
alpha = max(alpha, tableEval)
} else { // upperbound
beta = min(beta, tableEval)
}
if alpha >= beta {
return tableEval, tableMove
}
}
if depth == 0 {
//return eval.Evaluate(b), 0
return quiesce(b, alpha, beta, stop), 0
}
alpha0 := alpha
bestVal := int16(negInf)
moves := b.GenerateLegalMoves()
var bestMove dragontoothmg.Move
if len(moves) > 0 {
bestMove = moves[0] // 随机选择一些移动
}
for _, move := range moves {
unapply := b.Apply(move)
var score int16
score, _ = ab(b, -beta, -alpha, depth-1, halt, stop)
score = -score
unapply()
if score > bestVal {
bestMove = move
bestVal = score
}
alpha = max(alpha, score)
if alpha >= beta {
break
}
}
if *stop {
return bestVal, bestMove
}
var nodeType uint8
if bestVal <= alpha0 {
nodeType = transtable.UpperBound
} else if bestVal >= beta {
nodeType = transtable.LowerBound
} else {
nodeType = transtable.Exact
}
transtable.Put(b, bestMove, bestVal, depth, nodeType)
return bestVal, bestMove
}
func quiesce(b *dragontoothmg.Board, alpha int16, beta int16, stop *bool) int16 {
nodeCount++
if *stop {
return alpha
}
var standPat int16
found, _, evalresult, _, ntype := transtable.Get(b)
if found && ntype == transtable.Exact {
standPat = evalresult
} else {
standPat = eval.Evaluate(b)
transtable.Put(b, 0, standPat, 0, transtable.Exact)
}
if standPat >= beta {
return beta
}
if alpha < standPat {
alpha = standPat
}
moves := b.GenerateLegalMoves()
if len(moves) == 0 { // TODO(dylhunn): 那么和棋呢?
return negInf
}
for _, move := range moves {
if !isCapture(move, b) {
continue
}
unapply := b.Apply(move)
score := -quiesce(b, -beta, -alpha, stop)
unapply()
if score >= beta {
return beta
}
if score > alpha {
alpha = score
}
}
return alpha
}
func isCapture(m dragontoothmg.Move, b *dragontoothmg.Board) bool {
toBitboard := (uint64(1) << m.To())
return (toBitboard&b.White.All != 0) || (toBitboard&b.Black.All != 0)
}
英文:
I have implemented an alpha-beta search with quiescence search for my chess engine. However, in most positions, the quiescence search is taking 80-90% of the total execution time, as indicated by my profiler. Do I have a bug with my pruning?
I have included both the alpha-beta routine and the quiescence routine.
My quiescence search is based directly on this pseudocode.
// Perform the alpha-beta search.
func ab(b *dragontoothmg.Board, alpha int16, beta int16, depth int8, halt chan bool, stop *bool) (int16, dragontoothmg.Move) {
nodeCount++
if *stop {
return alpha, 0
}
found, tableMove, tableEval, tableDepth, tableNodeType := transtable.Get(b)
if found && tableDepth >= depth {
if tableNodeType == transtable.Exact {
return tableEval, tableMove
} else if tableNodeType == transtable.LowerBound {
alpha = max(alpha, tableEval)
} else { // upperbound
beta = min(beta, tableEval)
}
if alpha >= beta {
return tableEval, tableMove
}
}
if depth == 0 {
//return eval.Evaluate(b), 0
return quiesce(b, alpha, beta, stop), 0
}
alpha0 := alpha
bestVal := int16(negInf)
moves := b.GenerateLegalMoves()
var bestMove dragontoothmg.Move
if len(moves) > 0 {
bestMove = moves[0] // randomly pick some move
}
for _, move := range moves {
unapply := b.Apply(move)
var score int16
score, _ = ab(b, -beta, -alpha, depth-1, halt, stop)
score = -score
unapply()
if score > bestVal {
bestMove = move
bestVal = score
}
alpha = max(alpha, score)
if alpha >= beta {
break
}
}
if *stop {
return bestVal, bestMove
}
var nodeType uint8
if bestVal <= alpha0 {
nodeType = transtable.UpperBound
} else if bestVal >= beta {
nodeType = transtable.LowerBound
} else {
nodeType = transtable.Exact
}
transtable.Put(b, bestMove, bestVal, depth, nodeType)
return bestVal, bestMove
}
func quiesce(b *dragontoothmg.Board, alpha int16, beta int16, stop *bool) int16 {
nodeCount++
if *stop {
return alpha
}
var standPat int16
found, _, evalresult, _, ntype := transtable.Get(b)
if found && ntype == transtable.Exact {
standPat = evalresult
} else {
standPat = eval.Evaluate(b)
transtable.Put(b, 0, standPat, 0, transtable.Exact)
}
if standPat >= beta {
return beta
}
if alpha < standPat {
alpha = standPat
}
moves := b.GenerateLegalMoves()
if len(moves) == 0 { // TODO(dylhunn): What about stalemate?
return negInf
}
for _, move := range moves {
if !isCapture(move, b) {
continue
}
unapply := b.Apply(move)
score := -quiesce(b, -beta, -alpha, stop)
unapply()
if score >= beta {
return beta
}
if score > alpha {
alpha = score
}
}
return alpha
}
func isCapture(m dragontoothmg.Move, b *dragontoothmg.Board) bool {
toBitboard := (uint64(1) << m.To())
return (toBitboard&b.White.All != 0) || (toBitboard&b.Black.All != 0)
}
答案1
得分: 1
如果我正确理解你的代码,你正在搜索所有的捕获着法。为了节省工作,你可以剪枝那些无望的捕获着法。事实证明,着法非常糟糕以至于可以安全地跳过它们,所以这种技术是相当安全的。
例如,看看这个局面:
FEN:rnbqkbnr/pppppppp/8/8/8/8/1PP1PPP1/RNBQKBNR w KQkq - 0 1
有三个捕获着法:
- Rxa7
- Qxd7+
- Rxh7
假设引擎首先尝试了带有皇后的捕获着法。黑方有四种方式进行回击,但是任何一种着法都很可能导致截断。
例如,黑方走了Bxd7。现在白方在结果局面中有两个捕获着法,Rxa7或Rxh7。
在这里,大多数引擎会意识到白方已经在材料上落后(与beta相比),即使捕获一个兵也无济于事。因此,这两个车的捕获着法不太可能导致截断。
在这里,你当前的搜索仍然会继续搜索这些着法。检测到这种情况并跳过这些着法将节省很多工作。
还有其他的优化方法。例如,具有静态交换评估的强大引擎会立即看出Qxd7将赢得一个兵,但会失去皇后。由于这是一个糟糕的交易,引擎可以立即跳过这个着法。对于另外两个车的捕获着法也是一样。
当然,这其中存在着权衡。如果你剪枝得太过激进,最终会剪掉一些好着法。一般来说,我建议在正常搜索中花更多时间,而不是在静态搜索中,所以激进的剪枝应该是可以的。
英文:
If I read your code correctly, you are searching all captures. What you can do to save work, is to prune the hopeless captures move. It turns out that it is quite common that moves are so bad that it is safe to skip them, so the technique is quite safe.
For example, take a look at this position:
FEN: rnbqkbnr/pppppppp/8/8/8/8/1PP1PPP1/RNBQKBNR w KQkq - 0 1
There are three captures:
- Rxa7
- Qxd7+
- Rxh7
Let us assume the engine first tries the capture move with the queen. Black has four ways to capture back, but any of these moves will likely result in a cutoff.
For instance, black plays Bxd7. Now white has two captures in the resulting position, Rxa7 or Rxh7.
Here, most engines will recognize that white has already fallen back in material (in comparison to beta) that even capturing a pawn will not help. So, both of this rook captures are unlikely to result in a cutoff.
Here, your current search would still continue to search these moves. Detecting such cases and skipping these moves will save a lot of work.
There are further optimizations. For example, strong engines with static exchange evaluation will immediately see that Qxd7 will win one pawn but will loose the queen. As this is a bad trade, the engine can immediately skip this move. The same goes for the other two rook captures.
As always, there is a trade-off, though. If you prune too aggressively, you will eventually prune good moves, too. In general, I would recommend to spend more time in the normal search, not in the quiescence search, so aggressive pruning should be fine.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论