The wrong way

Discussion of chess software programming and technical issues.

Moderator: Ras

User avatar
Desperado
Posts: 879
Joined: Mon Dec 15, 2008 11:45 am

Re: The wrong way

Post by Desperado »

Henk wrote:ok thank you.

Well it can never be bad if your move generator is 10% faster. So all my perft tests will run 10% faster too and how often do I run perft tests.

...
Wrong :!:

1.
If the effort to reach that goal will be done at the wrong time it is definitively bad !
You want to produce sth., so be productive! Make sure it works and continue. The goal is to have a "complete" engine "working", nothing else.
Respectively with what you present about your programming skills and your understanding of chess programming, just accept it and continue...

2.
I can exactly tell you how often you need to run your perft tests. Answer: Until it passes the test successful without any reported bugs.
A common misinterpretation of "Perft" is today to measure "engine" speed.

* it will just give you an upper bound
* it is used to verify that the base components do work correctly.

The usage is mainly limited to the second point and the name perft - performance test does not really illustrate the main purpose of it.
As soon everything works, forget about it and continue.

At some point your data structure and your choosen implementation will not allow you to continue easily.
If you reach that point, where you realize "a lot" of things can only be implemented with a lot of workarounds, the time has come to rewrite the base.
You will need do rewrite your engine many times, but finish one first.
Henk
Posts: 7251
Joined: Mon May 27, 2013 10:31 am

Re: The wrong way

Post by Henk »

Adding switch statements to reduce number of shifts also does not help enough. Move generation for sliders without using bitboards seems to be faster and simpler for Skipper.

Code: Select all

...
            ulong curRooks = curPiecesBB & Board.Rooks;
            curPiecesBB &= ~curRooks;
            while (curRooks != 0)
            {

                var bit = curRooks & (~curRooks + 1);
                curRooks &= curRooks - 1;
                var location = ChessBoard[bit];


                var locationDirMovesL = location.LeftMoves;
                var locationDirMovesR = location.RightMoves;
                var locationDirMovesD = location.DownMoves;
                var locationDirMovesU = location.UpMoves;

                var occupiedDirMovesL = occupiers & locationDirMovesL;
                var occupiedDirMovesR = occupiers & locationDirMovesR;
                var occupiedDirMovesD = occupiers & locationDirMovesD;
                var occupiedDirMovesU = occupiers & locationDirMovesU; 

           ulong right;

            offset = ChessBoard.LASTCOLUMN - location.ColNr;
            switch (offset)
            {
                case 0:
                    right = 0;
                    break;
                case 1:
                    right = locationDirMovesR ^ (
                                    (
                                          occupiedDirMovesR << 1
                                    ) & locationDirMovesR
                                );
                    break;
                case 2:
                    right = locationDirMovesR ^ (
                                          (
                                                occupiedDirMovesR << 1
                                              | occupiedDirMovesR << 2

                                          ) & locationDirMovesR
                                      );
                    break;
                case 3:
                    right = locationDirMovesR ^ (
                                 (
                                       occupiedDirMovesR << 1
                                     | occupiedDirMovesR << 2
                                     | occupiedDirMovesR << 3

                                 ) & locationDirMovesR
                             );
                    break;
                case 4:
                    right = locationDirMovesR ^ (
                                                         (
                                                               occupiedDirMovesR << 1
                                                             | occupiedDirMovesR << 2
                                                             | occupiedDirMovesR << 3
                                                             | occupiedDirMovesR << 4

                                                         ) & locationDirMovesR
                                                     );
                    break;
                case 5:
                    right = locationDirMovesR ^ (
                                                         (
                                                               occupiedDirMovesR << 1
                                                             | occupiedDirMovesR << 2
                                                             | occupiedDirMovesR << 3
                                                             | occupiedDirMovesR << 4
                                                             | occupiedDirMovesR << 5
                                                      ) & locationDirMovesR
                                                     );
                    break;
                default:
                    right = locationDirMovesR ^ (
                                                          (
                                                                occupiedDirMovesR << 1
                                                              | occupiedDirMovesR << 2
                                                              | occupiedDirMovesR << 3
                                                              | occupiedDirMovesR << 4
                                                              | occupiedDirMovesR << 5
                                                              | occupiedDirMovesR << 6
                                                          ) & locationDirMovesR
                                                      );
                    break;

            }
...

                   var moveBitsR =
                    (
                         left
                       | right
                       | down
                       | up
                    ) & emptySquares;

                var mvDictR = colSign == ColorSign.White ? ((Field)location).WhiteRookMovesDict : ((Field)location).BlackRookMovesDict;
                while (moveBitsR != 0)
                {
                    var moveBit = moveBitsR & (~moveBitsR + 1);
                    moveBitsR &= moveBitsR - 1;
                    var move = mvDictR[moveBit];
                    move.Value = (int)(HistoryTable[move]);
                    moves.Add(move);
                }
..
User avatar
hgm
Posts: 28387
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: The wrong way

Post by hgm »

General move generation is always slower with bitboards. You can only benefit from bitboards if in many places you need to do very selective move generation, like captures of a certain class of pieces. You can then first make the selection with bitwise boolean operations on the bitboards, and only extract the moves you need. If you want to generate a list of all moves, it is very difficult to beat mailbox, as it takes next to nothing to generate a mailbox move. (Just add the step vector to the previous move, check the occupancy of the to-square, and add the move to the move list.)
Henk
Posts: 7251
Joined: Mon May 27, 2013 10:31 am

Re: The wrong way

Post by Henk »

You mean like for instance to test if king can be captured in a KingInCheck method ?
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: The wrong way

Post by bob »

hgm wrote:General move generation is always slower with bitboards. You can only benefit from bitboards if in many places you need to do very selective move generation, like captures of a certain class of pieces. You can then first make the selection with bitwise boolean operations on the bitboards, and only extract the moves you need. If you want to generate a list of all moves, it is very difficult to beat mailbox, as it takes next to nothing to generate a mailbox move. (Just add the step vector to the previous move, check the occupancy of the to-square, and add the move to the move list.)
If you think about it a minute, bit boards take no more work than that either. And you don't have to check occupancy in every direction either, just once per moving piece...
User avatar
hgm
Posts: 28387
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: The wrong way

Post by hgm »

Indeed. With mailbox this is a bit cumbersome. The most straightforward way would be to scan in all 8 orthogonal and diagonal directions away from the King to see what you hit, and then test if it is an enemy that can move far enough towards that King, and then check the 8 squares from which a Knight could check you for enemy Knight occupancy. That is a lot of testing, and althouh you could stop after detecting the first checker, usually there are none, so that you would have to do them all.

Faster therefore is to work from the piece list: loop through all Knights, and check if their relative location to the King is such that they check. There are only two Knights, (and often not even that), but there are eight places from which they could check you. For Pawns this is just the other way around, so there you are better off just checking if there are Pawns on the board.

For the slider checks you could also start from the piece list, as there are only 5 sliders, and most of those would not be on a ray though your King (which you can cheaply test), so that you have to test 5 pieces rather than make 8 potentially expensive ray scans. You then only have to scan rays for sliders that aim at your King, to see whether they are blocked.

For discovered checks there are furthermore cheap incremental methods, which just test for alignment of the King with the from-square of the latest move, and for checks by testing the relevant alignment of the to-square of that move with the King. So that you have to do the more expensive from-scratch test only for King moves to see whether they stumble into check. And as only a small fraction of the moves are King moves, this also helps to push down the cost of check testing to a negligible level.

Note that there are many different mailbox representations: board-only with piece types, board with piece numbers + piece list, or those that use advanced helper data structures to speed up things, like a view-distance table, which helps avoiding ray scans by immediately telling you how. How exactly you would do things depends on the exact representation, and to make things really fast usually requires a more complex representation. 'Mailbox' is really just a collective name for a wide variety of representations, and mailbox-bitboard hybrids can also have advantages (e.g. only keeping an 'occupied' bitboard next to a mailbox + piece list).
Sven
Posts: 4052
Joined: Thu May 15, 2008 9:57 pm
Location: Berlin, Germany
Full name: Sven Schüle

Re: The wrong way

Post by Sven »

Henk wrote:Adding switch statements to reduce number of shifts also does not help enough. Move generation for sliders without using bitboards seems to be faster and simpler for Skipper.

Code: Select all

...
            ulong curRooks = curPiecesBB & Board.Rooks;
            curPiecesBB &= ~curRooks;
            while (curRooks != 0)
            {

                var bit = curRooks & (~curRooks + 1);
                curRooks &= curRooks - 1;
                var location = ChessBoard[bit];


                var locationDirMovesL = location.LeftMoves;
                var locationDirMovesR = location.RightMoves;
                var locationDirMovesD = location.DownMoves;
                var locationDirMovesU = location.UpMoves;

                var occupiedDirMovesL = occupiers & locationDirMovesL;
                var occupiedDirMovesR = occupiers & locationDirMovesR;
                var occupiedDirMovesD = occupiers & locationDirMovesD;
                var occupiedDirMovesU = occupiers & locationDirMovesU; 

           ulong right;

            offset = ChessBoard.LASTCOLUMN - location.ColNr;
            switch (offset)
            {
                case 0:
                    right = 0;
                    break;
                case 1:
                    right = locationDirMovesR ^ (
                                    (
                                          occupiedDirMovesR << 1
                                    ) & locationDirMovesR
                                );
                    break;
                case 2:
                    right = locationDirMovesR ^ (
                                          (
                                                occupiedDirMovesR << 1
                                              | occupiedDirMovesR << 2

                                          ) & locationDirMovesR
                                      );
                    break;
                case 3:
                    right = locationDirMovesR ^ (
                                 (
                                       occupiedDirMovesR << 1
                                     | occupiedDirMovesR << 2
                                     | occupiedDirMovesR << 3

                                 ) & locationDirMovesR
                             );
                    break;
...
You could try the "dumb7fill" method (see also https://chessprogramming.wikispaces.com/Dumb7Fill for a thorough description). It is not as fast as magic bitboards, for instance, but probably faster than what you currently do, and it also requires only very few lines of code, so it might be a good compromise for your case. Example for a function to get all rook attacks "to the right" (or "to the east") from a given bitboard containing exactly one "1" indicating a rook's location ("bbEmpty" is the bitboard indicating all empty squares on the board):

Code: Select all

uint64_t eastAttacksFrom(uint64_t bbRook, uint64_t bbEmpty)
{
    bbEmpty &= ~BB_FILE_A;
    uint64_t flood = bbRook;
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |=           (bbRook << 1) & bbEmpty;
    return              (flood << 1) & ~BB_FILE_A;
}
So based on this example a function to get all rook attacks at once from a given square would look like that:

Code: Select all

uint64_t allRookAttacksFrom(uint sqr, uint64_t bbEmpty)
{
    uint64_t bbRook = (1 << sqr);
    return northAttacksFrom(bbRook, bbEmpty)
         | eastAttacksFrom (bbRook, bbEmpty)
         | southAttacksFrom(bbRook, bbEmpty)
         | westAttacksFrom (bbRook, bbEmpty);
}
That way you could replace this whole bunch of code:

Code: Select all

// starting here:
var locationDirMovesL = location.LeftMoves;
...
// up to here:
                   var moveBitsR =
                    (
                         left
                       | right
                       | down
                       | up
                    ) & emptySquares;
And please, ignore the tiny overhead of very few additional function calls. It would be simply not measurable. Consider instead the benefit you get from reusing these small functions (allRookAttacksFrom(), allBishopAttacksFrom()) at various places:
- capture move generator (AND result with occupied bitboard),
- non-capture move generator (AND result with empty bitboard),
- is-king-in-check function (start at king square, AND result with bitboard of opponent's diagonal pieces, do same for orthogonal),
- pin detection,
- mobility,
- ...

The main area improvement would be "better code", but only if you actually use that method at all those places listed above. Speedup may be a side effect of it but, again, don't focus too much on speedup now ... Then, once you are done with it, you can continue with more challenging parts of your program, for instance the search.

Just in case you are still interested in improving something that "cannot be improved anymore" ... ;-)
User avatar
hgm
Posts: 28387
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: The wrong way

Post by hgm »

Sven Schüle wrote: Example for a function to get all rook attacks "to the right" (or "to the east") from a given bitboard containing exactly one "1" indicating a rook's location ("bbEmpty" is the bitboard indicating all empty squares on the board):

Code: Select all

uint64_t eastAttacksFrom(uint64_t bbRook, uint64_t bbEmpty)
{
    bbEmpty &= ~BB_FILE_A;
    uint64_t flood = bbRook;
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |=           (bbRook << 1) & bbEmpty;
    return              (flood << 1) & ~BB_FILE_A;
}
The MSB is 'to the right' for you? Are you sure you don't mean >> here? For << you wouldn't have to make such a fuss, as this shifts in the direction of carry propagation.
Sven
Posts: 4052
Joined: Thu May 15, 2008 9:57 pm
Location: Berlin, Germany
Full name: Sven Schüle

Re: The wrong way

Post by Sven »

hgm wrote:
Sven Schüle wrote: Example for a function to get all rook attacks "to the right" (or "to the east") from a given bitboard containing exactly one "1" indicating a rook's location ("bbEmpty" is the bitboard indicating all empty squares on the board):

Code: Select all

uint64_t eastAttacksFrom(uint64_t bbRook, uint64_t bbEmpty)
{
    bbEmpty &= ~BB_FILE_A;
    uint64_t flood = bbRook;
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |= (bbRook = (bbRook << 1) & bbEmpty);
    flood |=           (bbRook << 1) & bbEmpty;
    return              (flood << 1) & ~BB_FILE_A;
}
The MSB is 'to the right' for you? Are you sure you don't mean >> here? For << you wouldn't have to make such a fuss, as this shifts in the direction of carry propagation.
Not related to MSB here. The given task is to find attacks "to the right" when looking at a chessboard with A1 being in the lower left corner, and also based on https://chessprogramming.wikispaces.com ... ileMapping. On the page I linked to above you can read how dumb7fill works. Search for "eastAttacks" there.
User avatar
hgm
Posts: 28387
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: The wrong way

Post by hgm »

But this is awful for left shifts. The natural carry propagation would do all that shifting and ORing in a single instruction (O-2R).

It might even be competitive to keep two copies of the board similar to rotated bitboard, but one rotated 180 degrees, so that you can do the O-2R trick in both directions. Especially if you do not store a whole board per memory cell, but just a ray. Even in 32-bit words you could store two copies of a ray of 15 squares, so it would work upto 15x15 boards. The board would be divided over many memory cells, but each move would only touch the 4 rays through the from- and the 8 rays through the to-square, so it isn't really important how many words the entire board takes.