sroelants / simbelmyne

A somewhat UCI compliant chess engine that could probably beat you.
GNU General Public License v3.0
15 stars 1 forks source link

Split up evaluation into incremental and non-incremental. #177

Closed sroelants closed 4 weeks ago

sroelants commented 5 months ago

The big chunks that we could pull out, as far as I can tell, are:

  1. Incremental eval
  2. Volatile eval
  3. Pawn structure

1. Incremental eval

These are the bits that should get updated on a per-move basis, because we might only have to do a fraction of the work if we know what move the player made. E.g., if it was a rook move, we don't have to recalculate the pawn structure, bishop pair, or king safety terms. Since we need to compute the running total on every move anyway, there is no point storing this in the TT! Examples: Pawn shield, Pawn storm, Rook on open file, Bishop pair, etc...

2. Volatile eval

This is the part of the evaluation that has to be recomputed on every single move, and doesn't really benefit from being included in the incremental part. Because it gets computed from scratch every single time, this would benefit from getting cached. Examples: Mobility, King zone attacks, King virtual mobility, etc...

3. Pawn structure eval

This is a bit of an in-between. It benefits from the incremental updates (No need to recompute the pawn structure eval when moving a bishop), but is also imminently cacheable, since the pawn structure tends to be far more static.

sroelants commented 5 months ago

I wonder if I should have Position be in charge of these? Change the structure of Position to be:

struct Position {
    board: Board,
    history: [ZHash; 100],
    hash: ZHash,
    pawn_hash: ZHash,

    // Eval specific stuff
    phase: u8,
    pawn_eval: Option<Score>,
    incremental_eval: Eval,
}

impl Position {
    pub fn incremental_eval(self) -> Score {}
    pub fn volatile_eval(self, tt: &mut TTable) -> Score {}
    pub fn pawn_eval(self, pawn_cache: &mut PawnCache) -> Score {}
}

The idea would be: if Position#play_move processes a pawn move, it clears the pawn structure eval stored on the position, so it needs to get recomputed. Then we can either compute from scratch, or re-use a cached result, and store the result back in the cache afterwards.

sroelants commented 4 months ago

I think I may have been overthinking it.

Instead of splitting the eval up into incremental and volatile, we simply only do the incremental work in every Position#play_move call, and do the "volatile" work when the search calls Eval::total.

That means we're not doing double work (when adding/removing), and we're getting the same speedup from TT hits as we would've if we'd only stored the volatile part.

Pawn cache might still be relevant, though, since we can cache those much more effectively. Though the relevance of the pawn cache only gets bigger when we start adding extra pawn-terms that might be used in other eval terms as well (pawn tropism? pawn attacks for mobility calculations?)