国际象棋:静态搜索主导运行时

huangapple go评论93阅读模式
英文:

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 &amp;&amp; tableDepth &gt;= 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 &gt;= 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) &gt; 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 &gt; bestVal {
			bestMove = move
			bestVal = score
		}
		alpha = max(alpha, score)
		if alpha &gt;= beta {
			break
		}
	}

	if *stop {
		return bestVal, bestMove
	}

	var nodeType uint8
	if bestVal &lt;= alpha0 {
		nodeType = transtable.UpperBound
	} else if bestVal &gt;= 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 &amp;&amp; ntype == transtable.Exact {
		standPat = evalresult
	} else {
		standPat = eval.Evaluate(b)
		transtable.Put(b, 0, standPat, 0, transtable.Exact)
	}
	if standPat &gt;= beta {
		return beta
	}
	if alpha &lt; 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 &gt;= beta {
			return beta
		}
		if score &gt; alpha {
			alpha = score
		}
	}
	return alpha
}

func isCapture(m dragontoothmg.Move, b *dragontoothmg.Board) bool {
	toBitboard := (uint64(1) &lt;&lt; m.To())
	return (toBitboard&amp;b.White.All != 0) || (toBitboard&amp;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.

huangapple
  • 本文由 发表于 2017年5月18日 07:46:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/44036416.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定