AndyGrant / Ethereal

Ethereal, a UCI Chess Engine by Andrew Grant
GNU General Public License v3.0
343 stars 79 forks source link

Why split move generation of quiet and noisy moves #169

Closed Aryan1508 closed 3 years ago

Aryan1508 commented 3 years ago

I was reading your code and I realized that when you generate an entire movelist, you first generate the noisy moves and then generate the quiet moves. This requires you to calculate a lot of stuff TWICE, such as the pieces for a side when you do Bitboard piecex = us & board.pieces[piecex], you can save 6 & operations just by calculating the moves together. All you need to do is calculate all the moves in one go, instead of separating noisy and quiet moves.

The part i am talking about in movegen.c

pseudo  = genAllNoisyMoves(board, pseudoMoves);
    pseudo += genAllQuietMoves(board, pseudoMoves + pseudo);

Here the implementation of both move-generating functions is very similar. Such as finding the correct piece-bitboards for a side through an AND operation. As such you do this twice which is in-efficient and should be replaced by possibly another function that takes the advantage of the fact that you don't need to separate the noisy and quiet moves, and does everything in one cycle.

AndyGrant commented 3 years ago

In that particular instance, sure. But the vast majority of move generation comes from the staged move generation methods defined in movepicker.x. In those cases, noisy moves often produce cutoffs in the search tree, thus saving us from generating any quiet moves. I can assure you that this piece-wise generation is a net speedup for actual gameplay. It would be a slowdown for a pure PERFT (generate all moves for each position just to count them), however.

Aryan1508 commented 3 years ago

@AndyGrant Yes but there aren't any cases where you need to generate legal + noisy moves? In those cases, you will notice a slowdown, which can be resolved without much work at all, just the combining the implementations of getNoisyMoves() and genQuietlMoves(), easy

AndyGrant commented 3 years ago

In practice, everything is done with staged move generation. I think you meant to type genQuietMoves(), but if not -- deferring legality checking until later is a speedup, even if the move is illegal and then tried. Take a look at movepicker.c, which follows the following order of move processing:

enum {
    STAGE_TABLE,
    STAGE_GENERATE_NOISY, STAGE_GOOD_NOISY,
    STAGE_KILLER_1, STAGE_KILLER_2, STAGE_COUNTER_MOVE,
    STAGE_GENERATE_QUIET, STAGE_QUIET,
    STAGE_BAD_NOISY,
    STAGE_DONE,
};

I could get you some stats if you don't buy it, or link you to the completed tests of the past, but captures cause enough cutoffs that generating them together is not optimal. Note that generating them together is not just a trivial savings of a few operations. It also increases the size of lists which must be sorted through and such.

Aryan1508 commented 3 years ago

@AndyGrant very sorry, I meant combining the implementation of noisy and quiet moves to avoid repeated-calculation of a few things. I didn't mean generating them together since yes, that would be stupid-

AndyGrant commented 3 years ago

I don't think you are folloing what I'm trying to say. The code you linked

pseudo  = genAllNoisyMoves(board, pseudoMoves);
pseudo += genAllQuietMoves(board, pseudoMoves + pseudo);

is not a realistic case in any conditions inside Ethereal during gameplay. Ethereal will generate the noisy moves, perform tens of thousands of subtree searches on them, potentially produce a cutoff, before exhausting all of them and going to generate quiet moves.

If you think I am wrong, or if I'm not understanding what you are suggesting, please code up a version and push it to a branch, and I will happily run it through OpenBench to test it.