Skip to content

Commit 788d9fd

Browse files
committed
Thread local (lockfree) hash, ported from acepck's PR to egaroucid
1 parent 0e4b404 commit 788d9fd

9 files changed

Lines changed: 234 additions & 42 deletions

File tree

src/endgame.c

Lines changed: 135 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ static int search_solve_4(Search *search, int alpha)
339339
#endif
340340

341341
/**
342-
* @brief Evaluate a position using a shallow NWS.
342+
* @brief Evaluate a position using a shallow NWS. (5..6 empties)
343343
*
344344
* This function is used when there are few empty squares on the board. Here,
345345
* optimizations are in favour of speed instead of efficiency.
@@ -444,7 +444,7 @@ static int search_shallow(Search *search, const int alpha, bool pass1)
444444
}
445445

446446
/**
447-
* @brief Evaluate an endgame position with a Null Window Search algorithm.
447+
* @brief Evaluate an endgame position with a Null Window Search algorithm. (7..9 empties)
448448
*
449449
* This function is used when there are still many empty squares on the board. Move
450450
* ordering, hash table cutoff, enhanced transposition cutoff, etc. are used in
@@ -455,7 +455,8 @@ static int search_shallow(Search *search, const int alpha, bool pass1)
455455
* @param alpha Alpha bound.
456456
* @return The final score, as a disc difference.
457457
*/
458-
int NWS_endgame(Search *search, const int alpha)
458+
459+
static int NWS_endgame_local(Search *search, const int alpha)
459460
{
460461
int score, ofssolid, bestscore;
461462
unsigned long long hash_code, solid_opp;
@@ -491,34 +492,32 @@ int NWS_endgame(Search *search, const int alpha)
491492
// Improvement of Serch by Reducing Redundant Information in a Position of Othello
492493
// Hidekazu Matsuo, Shuji Narazaki
493494
// http://id.nii.ac.jp/1001/00156359/
494-
if (search->eval.n_empties <= MASK_SOLID_DEPTH) { // (99%)
495-
solid_opp = full[4] & hashboard.opponent; // full[4] = all full
495+
solid_opp = full[4] & hashboard.opponent; // full[4] = all full
496496
#ifndef POPCOUNT
497-
if (solid_opp) // (72%)
497+
if (solid_opp) // (72%)
498498
#endif
499-
{
500-
hashboard.player ^= solid_opp; // normalize solid to player
501-
hashboard.opponent ^= solid_opp;
502-
ofssolid = bit_count(solid_opp) * 2; // hash score is ofssolid grater than real
503-
}
499+
{
500+
hashboard.player ^= solid_opp; // normalize solid to player
501+
hashboard.opponent ^= solid_opp;
502+
ofssolid = bit_count(solid_opp) * 2; // hash score is ofssolid grater than real
504503
}
505504
}
506505

507506
hash_code = board_get_hash_code(&hashboard);
508-
hash_prefetch(&search->hash_table, hash_code);
507+
PREFETCH(search->thread_hash.hash + (hash_code & search->thread_hash.hash_mask));
509508

510509
search_get_movelist(search, &movelist);
511510

512511
if (movelist.n_moves > 1) { // (96%)
513512
// transposition cutoff
514-
if (hash_get(&search->hash_table, &hashboard, hash_code, &hash_data.data)) { // (6%)
513+
if (hash_get_local(&search->thread_hash, &hashboard, hash_code, &hash_data.data)) { // (6%)
515514
hash_data.data.lower -= ofssolid;
516515
hash_data.data.upper -= ofssolid;
517516
if (search_TC_NWS(&hash_data.data, search->eval.n_empties, NO_SELECTIVITY, alpha, &score)) // (6%)
518517
return score;
519518
}
520519
// else if (ofssolid) // slows down
521-
// hash_get_from_board(&search->hash_table, HBOARD_V(board0), &hash_data.data);
520+
// hash_get_from_board(&search->thread_hash, HBOARD_V(board0), &hash_data.data);
522521

523522
movelist_evaluate_fast(&movelist, search, &hash_data.data);
524523

@@ -547,7 +546,7 @@ int NWS_endgame(Search *search, const int alpha)
547546
search->eval.parity = parity0 ^ QUADRANT_ID[move->x];
548547
empty_remove(search->empties, move->x);
549548
vboard_update(&search->board, board0, move);
550-
score = -NWS_endgame(search, ~alpha);
549+
score = -NWS_endgame_local(search, ~alpha);
551550
empty_restore(search->empties, move->x);
552551
search->board = board0.board;
553552

@@ -565,12 +564,12 @@ int NWS_endgame(Search *search, const int alpha)
565564

566565
hash_data.data.wl.c.depth = search->eval.n_empties;
567566
hash_data.data.wl.c.selectivity = NO_SELECTIVITY;
568-
hash_data.data.wl.c.cost = last_bit(search->n_nodes - nodes_org);
567+
// hash_data.data.wl.c.cost = last_bit(search->n_nodes - nodes_org);
569568
// hash_data.data.move[0] = bestmove;
570569
hash_data.alpha = alpha + ofssolid;
571570
hash_data.beta = alpha + ofssolid + 1;
572571
hash_data.score = bestscore + ofssolid;
573-
hash_store(&search->hash_table, &hashboard, hash_code, &hash_data);
572+
hash_store_local(&search->thread_hash, &hashboard, hash_code, &hash_data);
574573

575574
// special cases
576575
} else if (movelist.n_moves == 1) { // (3%)
@@ -581,12 +580,130 @@ int NWS_endgame(Search *search, const int alpha)
581580
vboard_update(&search->board, board0, move);
582581
if (--search->eval.n_empties <= DEPTH_TO_SHALLOW_SEARCH) // (56%)
583582
bestscore = -search_shallow(search, ~alpha, false);
584-
else bestscore = -NWS_endgame(search, ~alpha);
583+
else bestscore = -NWS_endgame_local(search, ~alpha);
585584
++search->eval.n_empties;
586585
empty_restore(search->empties, move->x);
587586
search->eval.parity = parity0;
588587
search->board = board0.board;
589588

589+
} else { // (1%)
590+
if (can_move(search->board.opponent, search->board.player)) { // pass
591+
search_pass(search);
592+
bestscore = -NWS_endgame_local(search, ~alpha);
593+
search_pass(search);
594+
} else { // game over
595+
bestscore = search_solve(search);
596+
}
597+
}
598+
599+
if (SQUARE_STATS(1) + 0) {
600+
foreach_move(move, movelist)
601+
++statistics.n_played_square[search->eval.n_empties][SQUARE_TYPE[move->x]];
602+
if (bestscore > alpha)
603+
++statistics.n_good_square[search->eval.n_empties][SQUARE_TYPE[bestscore]];
604+
}
605+
assert(SCORE_MIN <= bestscore && bestscore <= SCORE_MAX);
606+
assert((bestscore & 1) == 0);
607+
return bestscore;
608+
}
609+
610+
/**
611+
* @brief Evaluate an endgame position with a Null Window Search algorithm. (10..15 empties)
612+
*
613+
* This function is used when there are still many empty squares on the board. Move
614+
* ordering, hash table cutoff, enhanced transposition cutoff, etc. are used in
615+
* order to diminish the size of the tree to analyse, but at the expense of a
616+
* slower speed.
617+
*
618+
* @param search Search.
619+
* @param alpha Alpha bound.
620+
* @return The final score, as a disc difference.
621+
*/
622+
int NWS_endgame(Search *search, const int alpha)
623+
{
624+
int score, bestscore;
625+
unsigned long long hash_code;
626+
// const int beta = alpha + 1;
627+
HashStoreData hash_data;
628+
Move *move;
629+
long long nodes_org;
630+
V2DI board0;
631+
Board hashboard;
632+
unsigned int parity0;
633+
unsigned long long full[5];
634+
MoveList movelist;
635+
636+
assert(bit_count(~(search->board.player|search->board.opponent)) < DEPTH_MIDGAME_TO_ENDGAME);
637+
assert(SCORE_MIN <= alpha && alpha <= SCORE_MAX);
638+
639+
if (search->eval.n_empties <= DEPTH_TO_USE_LOCAL_HASH)
640+
return NWS_endgame_local(search, alpha);
641+
642+
if (search->stop) return alpha;
643+
644+
SEARCH_STATS(++statistics.n_NWS_endgame);
645+
SEARCH_UPDATE_INTERNAL_NODES(search->n_nodes);
646+
647+
// stability cutoff
648+
hashboard = board0.board = search->board;
649+
if (USE_SC && alpha >= NWS_STABILITY_THRESHOLD[search->eval.n_empties]) { // (7%)
650+
CUTOFF_STATS(++statistics.n_stability_try;)
651+
score = SCORE_MAX - 2 * get_stability(search->board.opponent, search->board.player);
652+
if (score <= alpha) { // (3%)
653+
CUTOFF_STATS(++statistics.n_stability_low_cutoff;)
654+
return score;
655+
}
656+
}
657+
658+
hash_code = board_get_hash_code(&hashboard);
659+
hash_prefetch(&search->hash_table, hash_code);
660+
661+
search_get_movelist(search, &movelist);
662+
663+
if (movelist.n_moves > 0) { // (96%)
664+
// transposition cutoff
665+
if (hash_get(&search->hash_table, &hashboard, hash_code, &hash_data.data)) { // (6%)
666+
if (search_TC_NWS(&hash_data.data, search->eval.n_empties, NO_SELECTIVITY, alpha, &score)) // (6%)
667+
return score;
668+
}
669+
if (movelist.n_moves > 1)
670+
movelist_evaluate_fast(&movelist, search, &hash_data.data);
671+
672+
nodes_org = search->n_nodes;
673+
parity0 = search->eval.parity;
674+
bestscore = -SCORE_INF;
675+
// loop over all moves
676+
move = &movelist.move[0];
677+
--search->eval.n_empties; // for next move
678+
while ((move = move_next_best(move))) { // (76%)
679+
search->eval.parity = parity0 ^ QUADRANT_ID[move->x];
680+
empty_remove(search->empties, move->x);
681+
vboard_update(&search->board, board0, move);
682+
score = -NWS_endgame(search, ~alpha);
683+
empty_restore(search->empties, move->x);
684+
search->board = board0.board;
685+
686+
if (score > bestscore) { // (63%)
687+
bestscore = score;
688+
hash_data.data.move[0] = move->x;
689+
if (bestscore > alpha) break; // (39%)
690+
}
691+
}
692+
++search->eval.n_empties;
693+
search->eval.parity = parity0;
694+
695+
if (search->stop) // (1%)
696+
return alpha;
697+
698+
hash_data.data.wl.c.depth = search->eval.n_empties;
699+
hash_data.data.wl.c.selectivity = NO_SELECTIVITY;
700+
hash_data.data.wl.c.cost = last_bit(search->n_nodes - nodes_org);
701+
// hash_data.data.move[0] = bestmove;
702+
hash_data.alpha = alpha;
703+
hash_data.beta = alpha + 1;
704+
hash_data.score = bestscore;
705+
hash_store(&search->hash_table, &hashboard, hash_code, &hash_data);
706+
590707
} else { // (1%)
591708
if (can_move(search->board.opponent, search->board.player)) { // pass
592709
search_pass(search);

src/hash.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,56 @@ void hash_print(const HashData *data, FILE *f)
707707
fprintf(f, "score = [%+02d, %+02d] ; ", data->lower, data->upper);
708708
fprintf(f, "level = %2d:%2d:%2d@%3d%%", data->wl.c.date, data->wl.c.cost, data->wl.c.depth, selectivity_table[data->wl.c.selectivity].percent);
709709
}
710+
711+
/**
712+
* @brief Thead local (lockfree), 1-way version of hash_store
713+
*
714+
* @param hash_table Hash table to update.
715+
* @param board Bitboard.
716+
* @param hash_code Hash code of an othello board.
717+
* @param storedata.alpha Alpha bound when calling the alphabeta function.
718+
* @param storedata.beta Beta bound when calling the alphabeta function.
719+
* @param storedata.score Best score found.
720+
* @param storedata.move Best move found.
721+
*/
722+
void hash_store_local(HashTable *hash_table, const Board *board, const unsigned long long hash_code, HashStoreData *storedata)
723+
{
724+
Hash *hash = hash_table->hash + (hash_code & hash_table->hash_mask);
725+
HashData *data = &hash->data;
726+
int score = storedata->score;
727+
if (board_equal(&hash->board, board)) { // data_update
728+
if (score < storedata->beta && score < data->upper) data->upper = score;
729+
if (score > storedata->alpha && score > data->lower) data->lower = score;
730+
// if (storedata->data.wl.c.cost > data->wl.c.cost)
731+
// data->wl.c.cost = storedata->data.wl.c.cost;
732+
} else { // data_new
733+
hash->board = *board;
734+
if (score < storedata->beta) data->upper = score; else data->upper = SCORE_MAX;
735+
if (score > storedata->alpha) data->lower = score; else data->lower = SCORE_MIN;
736+
data->wl = storedata->data.wl;
737+
data->move[0] = NOMOVE;
738+
}
739+
if (score > storedata->alpha || score == SCORE_MIN)
740+
data->move[0] = storedata->data.move[0];
741+
}
742+
743+
/**
744+
* @brief Thead local (lockfree), 1-way version of hash_get
745+
*
746+
* @param hash_table Hash table.
747+
* @param board Bitboard.
748+
* @param hash_code Hash code of an othello board.
749+
* @param data Output hash data.
750+
* @return True the board was found, false otherwise.
751+
*/
752+
bool hash_get_local(HashTable *hash_table, const Board *board, const unsigned long long hash_code, HashData *data)
753+
{
754+
Hash *hash = hash_table->hash + (hash_code & hash_table->hash_mask);
755+
if (board_equal(&hash->board, board)) {
756+
*data = hash->data;
757+
return true;
758+
} else {
759+
*data = HASH_DATA_INIT;
760+
return false;
761+
}
762+
}

src/hash.h

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,28 @@ bool hash_get_from_board(HashTable*, const Board *, HashData *);
100100
void hash_exclude_move(HashTable*, const Board *, const unsigned long long, const int);
101101
void hash_copy(const HashTable*, HashTable*);
102102
void hash_print(const HashData*, FILE*);
103+
void hash_store_local(HashTable *, const Board *, const unsigned long long, HashStoreData *);
104+
bool hash_get_local(HashTable*, const Board *, const unsigned long long, HashData *);
103105
extern unsigned int writeable_level(HashData *data);
104106

105107
extern const HashData HASH_DATA_INIT;
106108

109+
#ifdef hasSSE2
110+
#define PREFETCH(P) _mm_prefetch((char const *)(P), _MM_HINT_T0)
111+
#elif defined(__ARM_ACLE)
112+
#define PREFETCH(P) __pld(P)
113+
#elif defined(__GNUC__)
114+
#define PREFETCH(P) __builtin_prefetch(P)
115+
#elif defined(_M_ARM) || defined(_M_ARM64)
116+
#define PREFETCH(P) __prefetch(P)
117+
#else
118+
#define PREFETCH(P)
119+
#endif
120+
107121
inline void hash_prefetch(HashTable *hashtable, unsigned long long hashcode) {
108122
Hash *p = hashtable->hash + (hashcode & hashtable->hash_mask);
109-
#ifdef hasSSE2
110-
_mm_prefetch((char const *) p, _MM_HINT_T0);
111-
_mm_prefetch((char const *)(p + HASH_N_WAY - 1), _MM_HINT_T0);
112-
#elif defined(__ARM_ACLE)
113-
__pld(p);
114-
__pld(p + HASH_N_WAY - 1);
115-
#elif defined(__GNUC__)
116-
__builtin_prefetch(p);
117-
__builtin_prefetch(p + HASH_N_WAY - 1);
118-
#elif defined(_M_ARM) || defined(_M_ARM64)
119-
__prefetch(p);
120-
__prefetch(p + HASH_N_WAY - 1);
121-
#endif
123+
PREFETCH(p);
124+
PREFETCH(p + HASH_N_WAY - 1);
122125
}
123126

124127
#endif

src/midgame.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ int PVS_midgame(Search *search, const int alpha, const int beta, int depth, Node
841841
hash_store(&search->pv_table, &search->board, hash_code, &hash_data);
842842

843843
// store solid-normalized for endgame TC
844-
if (search->eval.n_empties <= depth && depth <= MASK_SOLID_DEPTH && depth > DEPTH_TO_SHALLOW_SEARCH) {
844+
if (search->eval.n_empties <= depth && depth <= DEPTH_TO_USE_LOCAL_HASH && depth > DEPTH_TO_SHALLOW_SEARCH) {
845845
solid_opp = get_all_full_lines(search->board.player | search->board.opponent) & search->board.opponent;
846846
if (solid_opp) {
847847
hashboard.player = search->board.player ^ solid_opp; // normalize solid to player
@@ -850,7 +850,7 @@ int PVS_midgame(Search *search, const int alpha, const int beta, int depth, Node
850850
hash_data.alpha += ofssolid;
851851
hash_data.beta += ofssolid;
852852
hash_data.score += ofssolid;
853-
hash_store(&search->hash_table, &hashboard, board_get_hash_code(&hashboard), &hash_data);
853+
hash_store_local(&search->thread_hash, &hashboard, board_get_hash_code(&hashboard), &hash_data);
854854
}
855855
}
856856

src/options.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
/** global options with default value */
2222
Options options = {
23-
22, // hash table size (2^22 * 24 * 1.125 = 113MB)
23+
21, // hash table size (2^21 * 24 * 1.125 = 57MB)
2424

2525
{0,-2,-3}, // inc_sort_depth
2626

src/search.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ const Selectivity selectivity_table [] = {
114114
/** threshold values to try stability cutoff during NWS search */
115115
// TODO: better values may exist.
116116
const signed char NWS_STABILITY_THRESHOLD[] = { // 99 = unused value...
117-
99, 99, 99, 99, 6, 8, 10, 12,
118-
8, 10, 20, 22, 24, 26, 28, 30, // 8 & 9 lowered to work best with solid stone
117+
99, 99, 99, 99, 6, 8, 8, 8,
118+
8, 10, 20, 22, 24, 26, 28, 30, // 6..9 lowered to work best with solid stone
119119
32, 34, 36, 38, 40, 42, 44, 46,
120120
48, 48, 50, 50, 52, 52, 54, 54,
121121
56, 56, 58, 58, 60, 60, 62, 62,
@@ -380,6 +380,12 @@ void search_init(Search *search)
380380
search->shallow_table.hash = NULL;
381381
search->shallow_table.hash_mask = 0;
382382
search_resize_hashtable(search);
383+
search->thread_hash.hash = mm_malloc(((1 << THREAD_LOCAL_HASH_SIZE) + HASH_N_WAY) * sizeof (Hash));
384+
if (search->thread_hash.hash == NULL) {
385+
fatal_error("Cannot allocate a thread hash\n");
386+
}
387+
search->thread_hash.hash_mask = (1 << THREAD_LOCAL_HASH_SIZE) - 1;
388+
hash_cleanup(&search->thread_hash);
383389

384390
/* board */
385391
search->board.player = search->board.opponent = 0;
@@ -460,7 +466,8 @@ void search_free(Search *search)
460466
hash_free(&search->pv_table);
461467
hash_free(&search->shallow_table);
462468
// eval_free(search->eval);
463-
469+
mm_free(search->thread_hash.hash);
470+
464471
task_stack_free(search->tasks);
465472
free(search->tasks);
466473
spin_free(search);

src/search.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ typedef struct Search {
7777
HashTable hash_table; /**< hashtable */
7878
HashTable pv_table; /**< hashtable for the pv */
7979
HashTable shallow_table; /**< hashtable for short search */
80+
HashTable thread_hash; /**< thread local hash */
8081
Random random; /**< random generator */
8182

8283
struct TaskStack *tasks; /**< available task queue */

0 commit comments

Comments
 (0)