knodes/s halved after improving eval

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

SrcEngine
Posts: 6
Joined: Sun Mar 22, 2020 3:04 pm
Full name: Luke Kong

knodes/s halved after improving eval

Post by SrcEngine »

I am new to chess programming. My engine used to run at 5000 knodes/s with a very simple eval. I've recently decided to improve the static evaluation functions. After adding no more than 100 lines of code, the engine is now only running at 2200knodes/s... Here is my updated EvalWhiteKnight function, which used to only evaluate based on a piecesquare table,

Code: Select all

int EvalWhiteKnight(const S_BOARD *pos)
{
	int pceNum;
	int index;
	int sq;
	int tempSq;
	int score=0;
//loop through all the knights
	for(pceNum = 0; pceNum < pos->pceNum[wN]; ++pceNum) {
		sq = pos->pList[wN][pceNum];
//knight mobility
		//loops through all the knight moves for the given knight
		for(index = 0; index < 8; ++index) {		
			tempSq = sq + KnDir[index];
			if(SqOnBoard(tempSq)) {
				//bonus if the piece controls one of the center squares
				if(tempSq == D4 || tempSq == D5 || tempSq==E4 || tempSq==E5)
				{
					score += knightCenterControl;		
				}
				//if the move is controlled by an enemy pawn, ignore the move 	
				if(pos->pieces[tempSq+11] != bP && pos->pieces[tempSq+9] != bP)
				//otherwise add mobility score to the knight
					score += knightMobility; 
			}
		}
		
//undefended minor piece penalty

		if(!SqAttacked(sq,WHITE,pos))
			score += undefendedMinor;
//knight outpost using bitmasks
		 //the knight needs to be in the enemy half of the board
		if(RanksBrd[sq]>RANK_4)
		{
			//no opposing black pawns to push the knight away, and the knight is supported by a white pawn
			if((pos->pawns[BLACK] & WhiteOutpostMask[SQ64(sq)])==0 && 
			(pos->pieces[sq-9]==wP || pos->pieces[sq-11]==wP))
			{
				//add bonus to this knight for having an outpost
				score += outpost;
			}
		}
	}
	return score;
}
I've changed my bishop evaluations as well, which also only used to evaluate based on piece square tables,

Code: Select all

int EvalWhiteBishop(const S_BOARD *pos)
{
	int pceNum;
	int sq;
	int index;
	int t_sq;
	int score=0;
//loop through all the white bishops
	for(pceNum = 0; pceNum < pos->pceNum[wB]; ++pceNum) {
		sq = pos->pList[wB][pceNum];
		//loop through all the bishop move directions
		for(index = 0; index < 4; ++index) {
			int dir = BiDir[index];
			t_sq = sq + dir;
			//loop through all the moves in each direction
			while(SqOnBoard(t_sq)) {
				//if the bishop is controlling the center, add bonus
				if(t_sq == D4 || t_sq == D5 || t_sq==E4 || t_sq==E5)
				{
					score += bishopCenterControl;		
				}
				//if the bishop is behind a black pawn chain, stop going through this direction			
				if(pos->pieces[t_sq]==bP) 
				{
					if(pos->pieces[t_sq+11] == bP || pos->pieces[t_sq+9] == bP) {
						break;
					}	
				}
				//similarly to white pawn chains
				if(pos->pieces[t_sq]==wP) 
				{
					if(pos->pieces[t_sq-11] == wP || pos->pieces[t_sq-9] == wP) {
						break;
					}	
				}

				t_sq += dir;

				//add mobility score to the bishop
				score += bishopMobility;
			}
		}
	}
	//bishop pair
	if(pos->pceNum[wB]>1)
		score += BishopPair;

	return score;	
}
Is this drastic speed reduction normal? Or have I messed something up terribly...? I could not pinpoint anything in my code that is crazy computationally expensive. I can't imagine what the speed is gonna be after I update all of my evaluation functions...
brianr
Posts: 536
Joined: Thu Mar 09, 2006 3:01 pm

Re: knodes/s halved after improving eval

Post by brianr »

Yes, it is expected that doing more in the eval will slow things down quite a lot.
What counts is how well it plays.
Mobility with computing moves and looking at squares attacked is quite expensive.
There were some posts at one point about the relative value of various mobility terms.
The baseline (fastest nps but worst play) was something like only piece values plus piece square table values.
Passed pawns are probably pretty important, IIRC.
Many eval terms only add a little bit (80/20 rule).
Experiment.
My engine Tinker's eval was always pretty poor, so endgame tablebases helped a lot and I did not bother to add much endgame knowledge. Another area that is up to you.

Suggest using a robust match testing methodology (repeat each position changing sides, etc).
Get a decent opening book and use a tool like cutechess-cli and Ordo.
Do a "sanity check" first playing two identical copies of your engine against each other (perhaps from separate directories). The results should be VERY close to 50/50.

Eval changes can be tested with fixed nodes per move games.
Search changes are generally tested with some time per move (which should be fast, but not too fast that there are time forfeits).

Caution: Computer chess can become addicting.
jorose
Posts: 358
Joined: Thu Jan 22, 2015 3:21 pm
Location: Zurich, Switzerland
Full name: Jonathan Rosenthal

Re: knodes/s halved after improving eval

Post by jorose »

Eval changes can be tested with fixed nodes per move games.
Just wanted to chime in here to mention that Brian probably is referring to small changes and tweaks to the evaluation function. Larger changes can effect how fast the engine can search nodes, as you described finding out yourself in this thread. In general I don't think its a bad idea to just always test with time and not nodes per move.
-Jonathan
brianr
Posts: 536
Joined: Thu Mar 09, 2006 3:01 pm

Re: knodes/s halved after improving eval

Post by brianr »

Yes, thanks for pointing this out.
Perhaps I've been spending too much time with Leela where nets of the same size can use fixed nodes.

Of course, if the new eval terms might take longer to score than the knowledge is worth, then timed games should be used. For A/B engines with IID surprisingly fast games and small increments can be used.
If the change is just a value in a table or a weight factor, then the fixed nodes option is reasonable.
User avatar
Kotlov
Posts: 266
Joined: Fri Jul 10, 2015 9:23 pm
Location: Russia

Re: knodes/s halved after improving eval

Post by Kotlov »

brianr wrote: Wed Jul 01, 2020 1:10 am Caution: Computer chess can become addicting.
confirm
Eugene Kotlov
Hedgehog 2.1 64-bit coming soon...
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: knodes/s halved after improving eval

Post by hgm »

If you calculate mobility by generating all moves, and checking some conditions after they would be made, it is almost like adding an extra ply to your search. Without increasing the node count. So of course your nodes/sec will take a hefty hit.

I can add that there would be more efficient methods for calculating what you do here. Whether Knight moves hit the center isn't dependent on any aspects of the position, and could have been incorporated in the piece-square table at engine startup, rather than recalculating it in every node. To know whether you want to count the Knight moves for the safe mobility requires you to check 2 board squares for Pawn presence, for each of 8 moves for each of 2 Knights, so 32 tests on the board in total. While there are at most 8 Pawns. If you would run through the Pawn list, using the location relative to that of a Kinght in a table lookup that tells you how many of the Knight moves that would attack, you would only need 8 x 2 = 16 tests. At worst; this would go down as Pawns get traded away, while the method you used would always keep checking 16 squares per Knight even when the opponent has no Pawns left at all.

For the Bishops it is a bit more tricky, as their moves can be blocked. So you cannot be sure whether these hit the center or a square under Pawn attack just by the location of the Bishop or the relative location to the Pawn. This seems to make explicit ray scanning like you do necessary. You could, however, keep that information at hand in a 'view-distance table' that you update incrementally: view[square][direction] would, for any occupied square, contain the distance to the next occupied square (or board edge) in each of the 8 directions. To get the mobility you then only have to add the view distances for the diagonal directions. (And to generate captures for that Bishop you would only have to test whether there is an enemy that distance away, without examining any intermediate board squares.)

Then you could also use a method similar to that for the Knight: run through the Pawn list, use the relative distance to the Bishops to look into a table if this makes the Pawn attack a square on a diagonal through the Bishop, and if so, in a second table, at what distance. Then you only have to compare that distance to the view distance of the Bishop in that direction to know whether you should discount some Bishop mobility. That might sound complex, but the secret is of course that you would hardly have to do any of it at all, as the first test (attacking the Bishops diagonal) would already fail for most Pawns. And you only have to test each Pawn against one Bishop, depending on the shade it is on. So if a Bishop on average would have 8 moves (like the Knight), instead of testing 2 x 8 x 2 = 32 board squares for Pawn presence, you would test relative Bishop location on 8 Pawns, and then occasionally do a bit extra. And again the work would go down as Pawns disappear.
SrcEngine
Posts: 6
Joined: Sun Mar 22, 2020 3:04 pm
Full name: Luke Kong

Re: knodes/s halved after improving eval

Post by SrcEngine »

Thank you all so much for taking the time to reply! I disliked my mobility code as well, but I didn't think it would impact the performance that much... I will replace the loops now with some lookup tables, and I will definitely incorporate some of the suggestions yall pointed out. :D