用位板识别棋子
Recognising a chess piece with bitboards
当棋盘存储在各种位板中时,现代国际象棋引擎如何识别特定单元格上的 type/side 棋子?我对此有疑问,因为要找出特定位的 type/side 是什么,我必须始终这样做:
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 数组是否会更快,它将固有地存储片段类型。
哪个更好,考虑到它必须是数百万次,以便在搜索功能中进行捕获分析。我做错了吗?
这是最慢的位板操作。但是,您很少需要执行它。
我看到你正在维护所有白色块的按位 'or' white
和所有黑色块的按位或 black
。使用这些,您可以快速拒绝移动到您自己的棋子上并轻松检测捕获。
在不太可能发生的捕获事件中,您必须测试 6 个敌方棋子位板中的最多 5 个,因为应该已经排除了捕获国王的可能性。而且,这并不像您想象的那么乏味;在 64 位系统上,每个掩码每个位板只有 1 个操作,然后是比较,所以 10 个整数操作。 And
/Or
是处理器上最轻的一些操作。单独维护 Piece[64]
比这花费更多时间。
我相信没有其他情况(在引擎代码中)需要从给定的方块中获取 pieceID。
位板的主要优点是移动生成和位置分析。没有什么可以比较的,所以无论如何你都会维护这个结构。
位校验本身很快;我主要担心分支。
相反,将 uint64_t bitboards[12]
视为所有片段的 12 个位板的数组。这现在在内存中是连续的,可以循环扫描:
for (int i = 0; i != 12; ++i)
{
if (bitboards[i] && bit_check) return i;
}
return -1; // empty.
只有两个分支(循环和检查)对于分支预测器来说更容易,连续内存优化了预取器。
明显的变化是检查 bitboards[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;
}
而不是 returning -1 为空,这将 return 12(标记值)。但是,这用更快的无条件分支代替了条件循环分支。这也意味着 return 值总是 int i
.
另一个不相关的优化是识别棋子是最常见的棋子,因此对白兵使用 bitboards[0]
对黑色棋子使用 bitboards[1]
或 bitboards[6]
更有效,取决于你是交织黑色还是白色碎片。
[编辑]
如果 color
有一个单独的位板,则不需要两个位板用于白色棋子和黑色棋子。取而代之的是,为棋子设置一个位板。检查黑色棋子和两个值。 (bit_check & color & bitboard[0]
)。要检查白色棋子,请反转颜色 (bit_check & ~color & bitboard[0]
)
当棋盘存储在各种位板中时,现代国际象棋引擎如何识别特定单元格上的 type/side 棋子?我对此有疑问,因为要找出特定位的 type/side 是什么,我必须始终这样做:
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 数组是否会更快,它将固有地存储片段类型。
哪个更好,考虑到它必须是数百万次,以便在搜索功能中进行捕获分析。我做错了吗?
这是最慢的位板操作。但是,您很少需要执行它。
我看到你正在维护所有白色块的按位 'or' white
和所有黑色块的按位或 black
。使用这些,您可以快速拒绝移动到您自己的棋子上并轻松检测捕获。
在不太可能发生的捕获事件中,您必须测试 6 个敌方棋子位板中的最多 5 个,因为应该已经排除了捕获国王的可能性。而且,这并不像您想象的那么乏味;在 64 位系统上,每个掩码每个位板只有 1 个操作,然后是比较,所以 10 个整数操作。 And
/Or
是处理器上最轻的一些操作。单独维护 Piece[64]
比这花费更多时间。
我相信没有其他情况(在引擎代码中)需要从给定的方块中获取 pieceID。
位板的主要优点是移动生成和位置分析。没有什么可以比较的,所以无论如何你都会维护这个结构。
位校验本身很快;我主要担心分支。
相反,将 uint64_t bitboards[12]
视为所有片段的 12 个位板的数组。这现在在内存中是连续的,可以循环扫描:
for (int i = 0; i != 12; ++i)
{
if (bitboards[i] && bit_check) return i;
}
return -1; // empty.
只有两个分支(循环和检查)对于分支预测器来说更容易,连续内存优化了预取器。
明显的变化是检查 bitboards[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;
}
而不是 returning -1 为空,这将 return 12(标记值)。但是,这用更快的无条件分支代替了条件循环分支。这也意味着 return 值总是 int i
.
另一个不相关的优化是识别棋子是最常见的棋子,因此对白兵使用 bitboards[0]
对黑色棋子使用 bitboards[1]
或 bitboards[6]
更有效,取决于你是交织黑色还是白色碎片。
[编辑]
如果 color
有一个单独的位板,则不需要两个位板用于白色棋子和黑色棋子。取而代之的是,为棋子设置一个位板。检查黑色棋子和两个值。 (bit_check & color & bitboard[0]
)。要检查白色棋子,请反转颜色 (bit_check & ~color & bitboard[0]
)