JohnWoe wrote: ↑Sat Oct 03, 2020 11:53 pm
I see this "C-hacker syndrome" going on. Where they write their buggy containers/string functions from scratch. Because they want to be "in control". I rather use std::vector/std::string and get the job done.
Linus Torvalds accepts new code to Kernel for 2 weeks then 8-10 weeks refactoring/bug fixing. The release cycle is about 10 weeks. The famous 80/20 ratio here.
Personally, in the past, I've also exhibited this behavior of wanting to write -everything- myself, and trying to keep code as short as possible.
For example, you can rewrite this:
Code: Select all
let x = a + b;
let y = d * r;
let z = x / y;
to this:
However, this begs the questions: what does (a + b) and (d * r) represent?" Obviously, imagine the single-letter variables to be more descriptive, or representing functions or something. It's only to demonstrate that you can put several lines into one line if you'd want to. Nowadays, the first example above, will be transformed to the second form by the compiler.
For example, my Square Attacked function is this:
Code: Select all
pub fn square_attacked(&self, board: &Board, attacker: Side, square: Square) -> bool {
let attackers = board.bb_pieces[attacker];
let occupancy = board.occupancy();
let bb_king = self.get_non_slider_attacks(Pieces::KING, square);
let bb_rook = self.get_slider_attacks(Pieces::ROOK, square, occupancy);
let bb_bishop = self.get_slider_attacks(Pieces::BISHOP, square, occupancy);
let bb_knight = self.get_non_slider_attacks(Pieces::KNIGHT, square);
let bb_pawns = self.get_pawn_attacks(attacker ^ 1, square);
let bb_queen = bb_rook | bb_bishop;
(bb_king & attackers[Pieces::KING] > 0)
|| (bb_rook & attackers[Pieces::ROOK] > 0)
|| (bb_queen & attackers[Pieces::QUEEN] > 0)
|| (bb_bishop & attackers[Pieces::BISHOP] > 0)
|| (bb_knight & attackers[Pieces::KNIGHT] > 0)
|| (bb_pawns & attackers[Pieces::PAWN] > 0)
}
It looks as if it first generates all of the attacks for all possible pieces, and then starts to check if one of them is true in the OR statement. This seems to waste a lot of calculation, because you could be calculating square attacks you don't need. This is probably the reason why I often see this function written with many if-statements, one for each piece, trying to cut the function short. In Rust however (in C as well, probably), the above function is transformed by the compiler:
Code: Select all
pub fn square_attacked(&self, board: &Board, attacker: Side, square: Square) -> bool {
(self.get_non_slider_attacks(Pieces::KING, square) & board.bb_pieces[attacker][Pieces::KING] > 0)
|| (self.get_slider_attacks(Pieces::ROOK, square, board.occupancy()) & board.bb_pieces[attacker][Pieces::ROOK] > 0)
|| ((self.get_slider_attacks(Pieces::ROOK, square, board.occupancy()) | bb_bishop = self.get_slider_attacks(Pieces::BISHOP, square, board.occupancy())) & board.bb_pieces[attacker][Pieces::QUEEN] > 0)
|| (bb_bishop = self.get_slider_attacks(Pieces::BISHOP, square, board.occupancy()) & board.bb_pieces[attacker][Pieces::BISHOP] > 0)
|| (self.get_non_slider_attacks(Pieces::KNIGHT, square) & board.bb_pieces[attacker][Pieces::KNIGHT] > 0)
|| (self.get_pawn_attacks(attacker ^ 1, square) & board.bb_pieces[attacker][Pieces::PAWN] > 0)
}
I might have made a copy/paste mistake somewhere, and the compiler will probably be able to reduce the function even further. Point is that all the 'extra' variables at the top will be elided by the compiler (to elide => omit something by merging it into something else), resulting in one huge OR-statement, which at runtime will be cut as short as possible.
Because of optimizations such as these, I prefer to write more descriptive code, using more lines, and more in-between variables. For example, my function that generates pawn moves is a massive row of let statements, building one on top of the previous, and it basically has only one command at the end to add a move to the move list:
Code: Select all
pub fn pawns(&self, board: &Board, list: &mut MoveList) {
const UP: i8 = 8;
const DOWN: i8 = -8;
let us = board.us();
let bb_opponent_pieces = board.bb_side[board.opponent()];
let bb_empty = !board.occupancy();
let bb_fourth = BB_RANKS[Board::fourth_rank(us)];
let direction = if us == Sides::WHITE { UP } else { DOWN };
let rotation_count = (NrOf::SQUARES as i8 + direction) as u32;
let mut bb_pawns = board.get_pieces(Pieces::PAWN, us);
// As long as there are pawns, generate moves for each of them.
while bb_pawns > 0 {
let from = bits::next(&mut bb_pawns);
let to = (from as i8 + direction) as usize;
let bb_push = BB_SQUARES[to];
let bb_one_step = bb_push & bb_empty;
let bb_two_step = bb_one_step.rotate_left(rotation_count) & bb_empty & bb_fourth;
let bb_targets = self.get_pawn_attacks(us, from);
let bb_captures = bb_targets & bb_opponent_pieces;
let bb_ep_capture = match board.game_state.en_passant {
Some(ep) => bb_targets & BB_SQUARES[ep as usize],
None => 0,
};
// Gather all moves for the pawn into one bitboard.
let bb_moves = bb_one_step | bb_two_step | bb_captures | bb_ep_capture;
self.add_move(board, Pieces::PAWN, from, bb_moves, list);
}
}
I don't even want to try and guess how the compiler is going to elide all of those variables, but be sure that it does. This way of coding, in combination with waterfall if-statements is a good way of getting descriptive code and avoiding
Rightward Drift, but id DOES use a lot more lines. (And thus requires more typing.)