Computer Chess Club Archives


Search

Terms

Messages

Subject: Re: Bitboards, pre-computing moves, and blocked squares?

Author: Gerd Isenberg

Date: 14:39:01 12/02/04

Go up one level in this thread


On December 02, 2004 at 14:20:40, Vasik Rajlich wrote:

>On December 02, 2004 at 12:51:28, Gerd Isenberg wrote:
>
>><snip>
>>>>Hi Vas,
>>>>
>>>>yes, you are right. In a rotated bitboard environment the x^(x-2) trick doesn't
>>>>makes sense, because a lookup already provides attacks in both opposite
>>>>directions on one ray for one particular sliding piece.
>>>>
>>>>In my current mmx-implementation i don't use rotated attack lookups anymore but
>>>>fillbased direction wise attack getters (Kogge-Stone and dumb7fill).
>>>>As you can see, the fillbased attack getters work with bitboard parameters
>>>>instead of a singular square index, and are able to generate attacks of sets,
>>>>multiple pieces (rooks/queen for straight directions, bishops/queen for diagonal
>>>>directions).
>>>>
>>>>Those fill based routines are great to get sets of taboo squares for the
>>>>opposite king and to get pinned pieces - and to get something like a progessive
>>>>mobility, e.g. a set of squares rooks may reach in two or even more moves etc..
>>>>
>>>>I use the x^(x-2) trick to substitute following Kogge-Stone fill-routine, which
>>>>saves a few cycles for this particular direction:
>>>>
>>>>// Kogge-Stone implementation by Steffan Westcott
>>>>
>>>>BitBoard getRightRookAttacks (BitBoard rooks, BitBoard empty)
>>>>{
>>>>  return shiftRight(fillRightOccluded(rooks, empty));
>>>>}
>>>>
>>>>BitBoard shiftRight (BitBoard b)
>>>>{
>>>>  return (b<<1) & 0xfefefefefefefefe;
>>>>}
>>>>
>>>>BitBoard fillRightOccluded(BitBoard g, BitBoard p)
>>>>{
>>>>  p &= 0xfefefefefefefefe;
>>>>  g |= p & (g << 1);
>>>>  p &= (p << 1);
>>>>  g |= p & (g << 2);
>>>>  p &= (p << 2);
>>>>  return g |= p & (g << 4);
>>>>}
>>>
>>>Aha. So, you have also something like:
>>>
>>>BitBoard getALLRookAttacks (BitBoard rooks, BitBoard empty)
>>>{
>>>return
>>>      getRightRookAttacks (rooks, empty) |
>>>      getLeftRookAttacks (rooks, empty) |
>>>      getUpRookAttacks (rooks, empty) |
>>>      getDownRookAttacks (rooks, empty) ;
>>>}
>>>
>>>And then you can do things like:
>>>
>>>Bitboard two_moves_away = getALLRookAttacks (getALLRookAttacks (rooks, empty),
>>>empty);
>>
>>
>>That's the idea.
>>
>>>
>>>etc ...
>>>
>>>I'd guess this is slower than a lookup for a one-bit bitboard, but more
>>>efficient once the mask starts to become more populated.
>>
>>Yes. I even tried it with single pupulated pieces (1ULL<<sq) in my former
>>rotated lookup approach. Queen attacks (eight directions) is IIRC something like
>>100 cycles with mmx-assembly (branchless and up to four mmx-instructions
>>simultaniously).
>>
>>Huge 64-bit register files are necessary (AMD64, MMX, SSE2, Altivec, Itanium).
>>Of couse it sucks on x86-32 with the few 32-bit gp-registers.
>>But of course, 1/1 replacement of rotated attack getters with fill-routines is
>>not the smartest way...
>>
>
>As far as I can see, in order to generate moves, you'll need to use the (1<<sq)
>bitboards. Maybe it makes sense to keep the lookup tables for move gen, and the
>fill routines when you need to "fill" starting with >1 bit.


That is a very reasonable approach - a pure fill approach requires some redesign
to become efficient.

With fill routines you are more in the pure bitboard centric world of sets
without the need of converting single bits into square metric by "dead slow"
bitscan ;-)

>
>>
>>>
>>>This might be an interesting tool to do some sort of "strategic reasoning". For
>>>example, pass (!pawns) into the second parameter, or even (! (very fixed
>>>pawns)), etc.
>>
>>Exactly - one may even "and" the "atacks in one set" with other taboo and
>>occupied by own sets...
>>
>
>Actually yes I can see a ton of ways to use this. The question is which ones are
>really helpful eval terms.


I guess there are a few conditional ones looking for trapped sliders and looking
for sliding trajectories against the opposite king or the ability to control far
stopp squares of passers in at least two moves etc.

But yes, difficult stuff.


>
>>>
>>>BTW I don't see how to get pins from this - unless you will manually unset bits
>>>in the "empty" bitboard ...
>>
>>No - you have only to consider the king as meta-slider as well and then "and"
>>opposite directions, e.g. for one of eight possible pin-directions:
>>
>>pinnedRem[RIGHT] = getRightRookAttacks (opprooks|oppqueens, empty)
>>                 & getLeftRookAttacks  (ownKing, empty);
>>
>>pinned[ownside][RIGHT] = pinnedRem[RIGHT] & ownPieces;
>>remChe[oppside][RIGHT] = pinnedRem[RIGHT] & oppPieces;
>>... all branchless stuff
>>
>
>Aha, that's nice. You even have the pinning direction.
>
>In fact you'd probably do:
>pinnedRem[RIGHT_LEFT] =
>   (getRightRookAttacks (opprooks|oppqueens, empty)
>  & getLeftRookAttacks  (ownKing, empty))
>|  (getLeftRookAttacks (opprooks|oppqueens, empty)
>  & getRightRookAttacks  (ownKing, empty))
>
>and ditto for UP_LEFT. I can't see a reason to keep RIGHT and LEFT separate.
>

hmm... not sure, the disjoint directions are easier to handle to find the
sliding pinner later. The popularity is never greater one - simpler, branchless
coding.

Of course in this LEFT_RIGHT case you can easily separate both direction wise
sets by masking with ownKBB-1 or ~(ownKbb-1). But extracting unified sets is not
often so simple, at least not so simple as to waste some memory keeping disjoint
sets and to unify by "or" on the fly if required.


>>In case of a sliding check on this direction, pinnedRem[RIGHT] contains a set of
>>empty squares between checking piece and checked king, which also is usefull.
>>With fillalgos it is important to keep disjoint directions.
>>
>
>True.
>
>>In opposite to Steffan i like to keep separated attacks for each sliding piece
>>as well - therefore i'm playing with a quad-bitboard approach.
>>The idea is to pass a quad-bitboard to fill-routines and to distinguish between
>>attacks of 15 different pieces.
>
>Ok here you lost me :)

hehe - i'm in the mood to explain it a bit,
also to reflect it by myself ;-)

Imagine this stange position occurs during search:

[D] 8/1R2rk2/8/3q4/8/8/r1R4K/6Q1 w - -

After there is no early return in search or qsearch, due to repetition, hash-hit
or lazy leaf cut (depth <= 0 && lazyOrHashedEva >= beta ),
i need attack maps for pinned piece detection, eval and (legal) movegen
purposes.

Two fills per direction ([RB]Q,K) for pinned piece detection.
Two additional fills (later for movegen) if we like to distinguish remove
checking moves.

In eval it is nice to have all eight attack sets piece wise, to get trapped
pieces. Single fills per piece and direction. Like rotated, one has to traverse
all eight man, including queen-like meta-king attacks as part of a kingsafety
term and determing sliding check targets), but one may safe the bitscan and only
isolate least significant one-bit by (x & -x).

The eval part may even be skipped as well, due to laziness or eval hash hit. The
relation of total nodes searched, lazy cut-nodes and full eval nodes is
interesting here.

So conditionally eight fills per direction for eval.

If no regulary eval leaf cut occurs, one has to traverse three white pieces for
sliding move generation. This might be implemented at once, or incrementally to
safe work in cut nodes.

Up to more three fills per direction here.

In best case there are two fills per dir with approach in mind, in worst case 8
or up to 2+8+2+3 = 15.

Instead of passing 8 or even 15 bitboards to one Kogge-Stone-routine, processing
n-times the empty propagator, i consider one quadword as kogge-stone generator.
The eight pieces above and empty square are coded in four bits eg. in following
manner:

0000 empty
0001 wR1
0010 wR2
0100 wQ
0111 wK
1001 bR1
1010 bR2
1100 bQ
1111 bK

Those bits are arranged vertically in four bitboards,
the quad bitboard for straight sliders:

     0123456789...........63
g0   00....0.1............0 // R1 or K
g1   00....0.0............0 // R2 or K
g2   00....1.0............0 // Q  or K
gb   00....0.1............0 // black
           | |
           | bK on a2
           wQ on g1

Now one quad-bitboard kogge-stone fill routine looks like

QuadBitBoard fillRightOccluded(QuadBitBoard g, BitBoard p)
{
  p    &= 0xfefefefefefefefe;
  g.g0 |= p & (g.g0 << 1);
  g.g1 |= p & (g.g1 << 1);
  g.g2 |= p & (g.g2 << 1);
  g.gb |= p & (g.gb << 1);
  p    &= (p << 1);
  g.g0 |= p & (g.g0 << 2);
  g.g1 |= p & (g.g1 << 2);
  g.g2 |= p & (g.g2 << 2);
  g.gb |= p & (g.gb << 2);
  p    &= (p << 2);
  g.g0 |= p & (g.g0 << 4);
  g.g1 |= p & (g.g1 << 4);
  g.g2 |= p & (g.g2 << 4);
  g.gb |= p & (g.gb << 4);
  return g;
}

with some nice prospects of parallel execution, specially considering parallel
work of different directions in disjoint execution units like 64-bit gp and/or
mmx as well as SSE2.

Disjoint piece attacks may easily be reconstrcuted on the fly with a few bitwise
operations.

The result of those routines is probably stored in some kind of thread-global
memory and therefore with limited validity during a search routine.

I even consider to do some parts of this stuff before probing the prefetched
main-tt to hide "huge" latencies and to have a better base to decide about lazy
eval and possible early mate or stalemate detection.

I hope you got the idea - problem with more than two rooks or more than one
queen per side. Special handling required for those rare case, since there is no
unique target, source relationship anymore of the direction wise attacks.

Diagonal "coding" is a bit easier since two bishops per side usually have
disjoint sets of squares.


>
>But thanks for reminding me why I put bitboards into my program.

No problem ;-)

Cheers,
Gerd

>
>Vas
>><snip>



This page took 0 seconds to execute

Last modified: Thu, 15 Apr 21 08:11:13 -0700

Current Computer Chess Club Forums at Talkchess. This site by Sean Mintz.