【问题标题】:Recognising a chess piece with bitboards用位板识别棋子
【发布时间】:2025-12-29 06:00:12
【问题描述】:

当棋盘存储在各种位板中时,现代国际象棋引擎如何识别特定单元格上的类型/边子?我对此有疑问,因为要找出特定位的类型/侧面,我必须始终这样做:

if((bit_check & occupied) == 0ULL) ... // empty
else if((bit_check & white) != 0ULL) // white
    if((bit_check & white_pawns) != 0ULL) ... // white pawns
    else if((bit_check & white_rooks) != 0ULL) ... // white rooks
    ....
    else if((bit_check & white_kings) != 0ULL) ... // white kings
else if((bit_check & black) != 0ULL) // black
    if((bit_check & black_pawns) != 0ULL) ... // black pawns
    ....
    else if((bit_check) & black_kings) != 0ULL) ... // black kings

这是一个相当乏味的过程,并且必须执行多次(例如,在移动生成期间查看正在捕获的内容)。我不确定我是否应该这样做,或者简单地创建一个类型为Piece[64] 的 64 位数组是否会更快,这将固有地存储片段类型。

考虑到必须进行数百万次才能在搜索功能中进行捕获分析,这会更好。我做错了吗?

【问题讨论】:

  • 如何将该测试级联放入一个单独的函数中,而不是在需要时重复代码?
  • @πάνταῥεῖ 我知道,这是关于操作时间,而不是外观。

标签: c++ c++11 if-statement chess bitboard


【解决方案1】:

位检查本身很快;我主要担心的是分支。

相反,将uint64_t bitboards[12] 视为所有部分的 12 个位板的数组。这现在在内存中是连续的,可以循环扫描:

for (int i = 0; i != 12; ++i)
{
  if (bitboards[i] && bit_check) return i;
}
return -1; // empty.

分支预测器只有两个分支(循环和检查)更容易,并且连续内存优化了预取器。

明显的变化是检查位板 [0] 到 [5] 仅用于白色棋子,而 [6] 到 [11] 仅用于黑色棋子。

更微妙的变体:

uint64_t bitboards[13];
bitboards[12] = ~uint64_t(0);
for (int i = 0; /* NO TEST*/ ; ++i)
{
     if (bitboards[i] && bit_check) return i;
}

这将返回 12(标记值),而不是为空返回 -1。但是,这用更快的无条件分支替换了条件循环分支。这也意味着返回值总是int i

另一个不相关的优化是识别棋子是最常见的棋子,因此对白棋子使用bitboards[0] 对黑棋子使用bitboards[1]bitboards[6] 更有效,具体取决于你是交错黑色还是白色件。

[编辑] 如果您有一个单独的用于color 的位板,则您不需要两个用于白色棋子和黑色棋子的位板。取而代之的是,为棋子设置一个位板。检查黑色棋子和两个值。 (bit_check & color & bitboard[0])。要检查白色棋子,请反转颜色 (bit_check & ~color & bitboard[0])

【讨论】:

  • 我已经使用了您提到的简单优化。也就是说,我首先检查它是否为空,然后是棋子,然后是车、骑士、主教、皇后,最后是国王。尽管如此,我不明白我怎么会错过它。很好的解决方案。
  • 顺便说一句,我认为您的意思是 i++。 bitboards[0] 根本没有用 ++i! 进行检查!
  • @ShreyasVinod:你可能想再次拿出你的 C++ 书。循环内使用的表达式只是简单的i。而for循环忽略了第三部分的值;它只检查中间部分的布尔值。我可以写for(int i = 0; i != 12; ++i * 7),它会做同样的事情。将未使用的值乘以 7 仍然使其未使用。但是,如果您不相信我,请在循环中添加 std::cout << i << std::endl;
  • 呵呵,有趣,你说得对,我的错。我希望它在条件检查之前增加。
  • 首先,您实际上不需要在一个循环中检查所有 12 个位板...实际上,这是一个 0、1、2、3、4 的循环。我只是展开它,但即使你没有优化器会,所以避免条件实际上并没有帮助。
【解决方案2】:

这是位板最慢的操作。但是,您很少需要执行它。

我看到您正在维护所有白色棋子的按位“或”white 和所有黑色棋子的按位“或”black。使用这些,您可以快速拒绝移动到自己的棋子上并轻松检测捕获。

在不太可能发生的捕获事件中,您必须测试 6 个敌方位板中的最多 5 个,因为应该已经排除了国王捕获。此外,这并不像您想象的那么乏味;在 64 位系统上,每个掩码仅是每个位板 1 次操作,然后是比较,因此 10 次整数操作。 And/Or 是处理器上最轻量级的一些操作。单独维护Piece[64] 会花费更多时间。

我相信没有其他情况(在引擎代码中)需要从给定的方格中获取pieceID。

位板的主要优点是移动生成和位置分析。没有什么可以比较的,所以无论如何你都会保持这个结构。

【讨论】:

  • 如果您查看代码,whiteblack 已经在其中。我同意,按位运算对于现代 ALU 来说是轻而易举的事。
  • 嗯,稍微想一想,你说的很对,因为在招式生成过程中,捕获很少见。
  • 我不确定是否有一个用于“所有白块”的位板是否有效,因为它是简单生成的。
  • 是的,我用不同的方式测量了它。它作为一些早期出局和其他情况的一部分被阅读,也在位置分析代码中被阅读。