Over the past six months, I have been developing a chess engine using the Zig programming language. It is already quite strong and has significant potential for further improvement, with a goal to reach an ELO strength of over 3000.
One of my objectives in creating this chess engine was to learn a new programming language. Initially, I started with C#, which I occasionally use for my job. However, I decided to learn something completely new. I experimented with both Rust and Zig, and ultimately chose Zig as it seemed more relevant to my work. I am electrical engineer and I also work with embedded systems. Coming from a background in C, Zig felt more intuitive to me compared to Rust.
Until now, I have been able to learn from resources available on the internet without needing to ask specific questions. However, at this point, I have quite a lot of things that bother me so I will be asking a number of questions.
First one is related to allocators and array.
My first question is related to allocators and arrays. One of the key components in a chess engine is the hash table of positions, which is built during the search for the best move. This table stores a nearly unique hash key for each position, the best move in that position, the evaluation score, and some other data. During the search, you can arrive at the same positions via different sequences of moves, and the search can take some time. Therefore, it is highly beneficial to store the best move and evaluation of each position. This way, you can probe the hash table and avoid repeating searches in positions that have already been explored. This can save a significant amount of time, which can then be used to perform deeper searches, which improves the strength of the chess engine.
One problem or bug I am encountering is related to how I reinitialize the hash table when I set its size. It seems that I am using twice the amount of RAM as the size of the hash table. While this does not affect the engine’s strength, it becomes an issue when I submit the engine for competitive play. All engines need to have a hash table of the same size, as a larger hash size increases the strength of the engine. The size of the hash table is correct, but the chess gui shows a large ruse of RAM memory, which could imply that my has table is actually bigger.
The code for setting the new size is as follows:"
pub fn init(self: *TranspositionTable, size_mb: u64) void {
self.ttArray.deinit();
var size: u64 = 1 << NB_BITS;
const MB: u64 = 1 << 20;
var nb_bits: u5 = 16;
if (size_mb > 0) {
while (((@as(u64, 1) << nb_bits) * @sizeOf(scoreEntry) <= size_mb * MB / 2) and (nb_bits < 29)) : (nb_bits += 1) {}
size = @as(u64, 1) << nb_bits;
}
std.debug.print("Hash size in MB: {}\n", .{size * @sizeOf(scoreEntry) / MB});
std.debug.print("Hash size in bytes: {}\n", .{size * @sizeOf(scoreEntry)});
var tt = TranspositionTable{
.ttArray = std.ArrayList(u128).init(tt_allocator.allocator()),
.size = size,
.mask = size - 1,
.age = 0,
};
tt.ttArray.ensureTotalCapacity(tt.size) catch {};
tt.ttArray.expandToCapacity();
self.* = tt;
self.clear();
}
The ScoreEntry is a packed structure that is 128 bits in size. The TranspositionTable structure contains ttArray, which consumes the majority of the memory. The rest of the structure contains information about the size, mask, and age. I use std.heap.c_allocator for its speed.
At the start, I deinitialize the old ttArray, create a new one with a new size, and set the pointer to the new array. I suspect that the old array is not being “erased”, but instead continues to consume the original amount of RAM, while the newly initialized array uses additional memory.
For example, I start with a hash table of 128 MB, then the GUI reinitializes the size with an additional 64 MB. When I set the size to a 256 MB array, the total memory used ends up being 128 + 64 + 256 MB, which is almost twice the actual required size. My understanding of deinit is that it clears and frees the memory, effectively reducing the memory used to 0 MB. I then reserve the correct size of new memory and use that. However, it seems my understanding is incorrect.
The question here is where exactlly is my understanding wrong and can you help me with some usefull examples.
Code for the engine is on https://github.com/jabolcni/Lambergar and code for hash table is https://github.com/jabolcni/Lambergar/blob/main/src/tt.zig