You could still use general code (i.e. not dedicated to a specific piece type and color) for the non-special moves, and put all case labels for such moves above that code. That should greatly improve the predictability of the branch instruction that implements the switch: it would always predict execution of that code, and it would only be wrong in the rare case the move was special. By executing different code for each piece type, the probability it would correctly predict what code is to be executed after the branch becomes very small.Mike Sherwin wrote: ↑Fri May 12, 2023 8:29 am I use a switch on piece type because I have pseudo piece types and even pseudo move types.
Here they are enumerated....Code: Select all
enum { OO, WP, WN, WB, WR, WRC, WQ, WK, WC, BP, BN, BB, BR, BRC, BQ, BK, BC, Wd, We, Wb, Wr, Wn, Wq, Bd, Be, Bb, Br, Bn, Bq, WS, WL, BS, BL };
You have an extra 'type' field in your move representation, which does double duty of distinguishing special moves, as well as identifying the type of the moved piece. The latter is essentially redundant information, as you could have read it from the board (like you do for ctype) just as easily as from the move.
By encoding 'virgin' pieces as a different type you essentially introduced extra 'promotions'. If you would use m->type to indicate the 'promotion piece' rather than the moving piece (as it in a sense already does for the true promotions) you could always do
board[m->ts] = m->type;
That way the KC, RC and d cases could also be grouped with the non-special moves.
It is true that the d case distinguishes itself from the non-special moves in that it also needs to set the e.p. square. There might be a more efficient solution for this, though. The move generator would know when it was generating a double push, and could set a special flag bit in the move representation when it does that, outside the 'type' field that you switch on. If the daughter node would be aware of the move that led to it, it could examine that flag at the time it asks itself whether it has to generate an e.p. capture or not; there would be no need to set it in MakeMove through a separate switch case or dedicated test. Your MakeMove code only sets epbb[ply+1] for moving a virgin Pawn, but of course it would need to clear it for all other moves. Just passing m to the daughter is just as easy as passing it epbb, and the move generator of the daughter would then test the 'double-push flag' in m to see if it must attempt to generate an e.p. capture (m->ts telling it what to capture and where). Which is probably cheaper than wondering for every diagonal Pawn move whether it goes to the e.p. square.
That only leaves castlings, promotions and e.p. captures, all extremely rare. So having separate cases for those in the switch (which would always be mispredicted when they actually occur) probably won't measurably hurt. All these cases need to do something extra (moving the Rook, clearing the e.p. square or adjusting the material key). And considering how rare they are, it would probably be a bad idea to always do these things (in a dummy way) for all other moves, just to save the branch mispredict associated with distinguishing the cases.
The We case indeed is a good way to treat e.p. capture and normal Pawn moves the same way. But this is a bit wasted on your code, as you would only get to the We case if you already know you are dealing with an e.p. capture; otherwise you would have gone to the WP case. So you might as well have written
sq = m->ts - 8;
(Note that in a color-agnostic way this could be written as sq = m->ts ^ 8;.)