Hi Niel,
lockless hashing is the way to go and it is much more simple than you might think. I try to explain...
Here is an example how the simple (single threaded) write operation can look like.
Then you will see the difference between lockless hashing directly after that.
Code: Select all
void TT::write(pos_t* pos, int move, int score, int depth, int ply, int flag)
{
hashslot_t* slot = &table[pos->key & (entries - 1)];
if(depth >= slot->data.depth || age != slot->data.age)
{
slot->data.depth = depth;
slot->data.age = age;
slot->data.flag = flag;
slot->data.move = move;
slot->data.score = scoreIntoSlot(score, ply);
slot->key = pos->key;
}
}
Now the lockless version. After that i will explain the difference.
Code: Select all
void TT::write(pos_t* pos, int move, int score, int depth, int ply, int flag)
{
hashslot_t* slot = &table[pos->key & (slotcount - 1)];
uint64_t* data = (uint64_t*) &slot->data;
if(depth >= slot->data.depth || age != slot->data.age) {
// SAME AS BEFORE BUT...
slot->key = pos->key ^ *data; // lockless hashing
}
}
As you can see only the following lines have changed...
Code: Select all
1. uint64_t* data = (uint64_t*) &slot->data;
2. slot->key = pos->key ^ *data;
The data structure behind looks like this
Code: Select all
typedef struct {
uint16_t move;
int16_t score;
int16_t unused;
uint16_t depth : 7, age : 7, flag : 2;
} hashdata_t;
typedef struct {
uint64_t key;
hashdata_t data;
} hashslot_t;
Now, the point is if different threads write different data into the slot, the stored key will be different.
When you retrieve the data it works like this.
Code: Select all
hashslot_t* hslot = TT::get_link(pos);
uint64_t* data = (uint64_t*) &hslot->data;
if(hslot->key == (pos->key ^ *data)) {
// lookup
}
If there were different threads writing different data at the same time, the current key of the position xored with
stored data will not result in the stored key.
The principle can be applied to larger data (more then 64 bits) too. But in the application context 64 bits allow an atomic
read and write operation and you can handle the data in one run.
To summarize, you xor in the data to the key when writing the data.
When reading the data, the current position key xored with the stored data must result in the stored key.
(Otherwise the data was from another thread with a different key)
Hope it helps a little bit.