Skip to content

Commit 520fc1d

Browse files
committed
optimize endgame_NWS_local for thread local transposition table
1 parent 788d9fd commit 520fc1d

File tree

11 files changed

+172
-205
lines changed

11 files changed

+172
-205
lines changed

src/board.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
*
44
* Board management header file.
55
*
6-
* @date 1998 - 2024
6+
* @date 1998 - 2025
77
* @author Richard Delorme
8+
* @author Toshihiko Okuhara
89
* @version 4.5
910
*/
1011

@@ -241,4 +242,13 @@ extern unsigned char edge_stability[256 * 256];
241242
#define vboard_get_moves(vboard) get_moves((vboard).board.player, (vboard).board.opponent)
242243
#endif
243244

245+
#if defined(__AVX__) || defined(__SSE4_1__)
246+
inline int vectorcall vboard_equal(V2DI v, Board *b) {
247+
__m128i t = _mm_xor_si128((v).v2, _mm_loadu_si128((__m128i *) (b)));
248+
return _mm_testz_si128(t, t);
249+
}
250+
#else
251+
#define vboard_equal(v,b) board_equal(&(v).board, (b))
252+
#endif
253+
244254
#endif

src/endgame.c

Lines changed: 71 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -436,20 +436,20 @@ static int search_shallow(Search *search, const int alpha, bool pass1)
436436
} while ((prioritymoves = moves)); // (23%)
437437
++search->eval.n_empties;
438438
}
439-
// search->board = board0.board;
439+
// search->board = board0.board; // restore in caller
440440
// search->eval.parity = parity0;
441441

442442
assert(SCORE_MIN <= bestscore && bestscore <= SCORE_MAX);
443443
return bestscore; // (33%)
444444
}
445445

446446
/**
447-
* @brief Evaluate an endgame position with a Null Window Search algorithm. (7..9 empties)
448-
*
449-
* This function is used when there are still many empty squares on the board. Move
450-
* ordering, hash table cutoff, enhanced transposition cutoff, etc. are used in
451-
* order to diminish the size of the tree to analyse, but at the expense of a
452-
* slower speed.
447+
* @brief Evaluate an endgame position with a Null Window Search algorithm,
448+
* with thread-local lockfree hash. (7..10 empties)
449+
450+
* Lightweight transposition table with thread local (thus lockfree) 1-way hash
451+
* is used for 7..10 empties (and occasionally less from PVS).
452+
* http://www.amy.hi-ho.ne.jp/okuhara/edaxopt.htm#localhash
453453
*
454454
* @param search Search.
455455
* @param alpha Alpha bound.
@@ -458,29 +458,27 @@ static int search_shallow(Search *search, const int alpha, bool pass1)
458458

459459
static int NWS_endgame_local(Search *search, const int alpha)
460460
{
461-
int score, ofssolid, bestscore;
462-
unsigned long long hash_code, solid_opp;
461+
int score, ofssolid, bestmove, bestscore, lower, upper;
462+
unsigned long long solid_opp;
463463
// const int beta = alpha + 1;
464-
HashStoreData hash_data;
464+
Hash *hash_entry;
465465
Move *move;
466-
long long nodes_org;
467-
V2DI board0;
468-
Board hashboard;
466+
// long long nodes_org;
467+
V2DI board0, hashboard;
469468
unsigned int parity0;
470469
unsigned long long full[5];
471470
MoveList movelist;
472471

473-
assert(bit_count(~(search->board.player|search->board.opponent)) < DEPTH_MIDGAME_TO_ENDGAME);
472+
assert(bit_count(~(search->board.player|search->board.opponent)) < DEPTH_TO_USE_LOCAL_HASH);
474473
assert(SCORE_MIN <= alpha && alpha <= SCORE_MAX);
475474

476-
if (search->stop) return alpha;
477-
478475
SEARCH_STATS(++statistics.n_NWS_endgame);
479476
SEARCH_UPDATE_INTERNAL_NODES(search->n_nodes);
480477

481478
// stability cutoff
482-
hashboard = board0.board = search->board;
479+
board0.board = hashboard.board = search->board;
483480
ofssolid = 0;
481+
// search_SC_NWS(search, alpha, &score)
484482
if (USE_SC && alpha >= NWS_STABILITY_THRESHOLD[search->eval.n_empties]) { // (7%)
485483
CUTOFF_STATS(++statistics.n_stability_try;)
486484
score = SCORE_MAX - 2 * get_stability_fulls(search->board.opponent, search->board.player, full);
@@ -492,99 +490,83 @@ static int NWS_endgame_local(Search *search, const int alpha)
492490
// Improvement of Serch by Reducing Redundant Information in a Position of Othello
493491
// Hidekazu Matsuo, Shuji Narazaki
494492
// http://id.nii.ac.jp/1001/00156359/
495-
solid_opp = full[4] & hashboard.opponent; // full[4] = all full
496-
#ifndef POPCOUNT
493+
solid_opp = full[4] & hashboard.board.opponent; // full[4] = all full
494+
#ifndef POPCOUNT
497495
if (solid_opp) // (72%)
498-
#endif
496+
#endif
499497
{
500-
hashboard.player ^= solid_opp; // normalize solid to player
501-
hashboard.opponent ^= solid_opp;
498+
#ifdef hasSSE2
499+
hashboard.v2 = _mm_xor_si128(hashboard.v2, _mm_set1_epi64x(solid_opp));
500+
#else
501+
hashboard.board.player ^= solid_opp; // normalize solid to player
502+
hashboard.board.opponent ^= solid_opp;
503+
#endif
502504
ofssolid = bit_count(solid_opp) * 2; // hash score is ofssolid grater than real
503505
}
504506
}
505507

506-
hash_code = board_get_hash_code(&hashboard);
507-
PREFETCH(search->thread_hash.hash + (hash_code & search->thread_hash.hash_mask));
508+
hash_entry = search->thread_hash.hash + (board_get_hash_code(&hashboard.board) & search->thread_hash.hash_mask);
509+
// PREFETCH(hash_entry);
508510

509511
search_get_movelist(search, &movelist);
510512

511-
if (movelist.n_moves > 1) { // (96%)
513+
if (movelist.n_moves > 0) {
512514
// transposition cutoff
513-
if (hash_get_local(&search->thread_hash, &hashboard, hash_code, &hash_data.data)) { // (6%)
514-
hash_data.data.lower -= ofssolid;
515-
hash_data.data.upper -= ofssolid;
516-
if (search_TC_NWS(&hash_data.data, search->eval.n_empties, NO_SELECTIVITY, alpha, &score)) // (6%)
517-
return score;
515+
// hash_get(&search->thread_hash, &hashboard.board, hash_code, &hash_data.data)
516+
unsigned char hashmove[2] = { NOMOVE, NOMOVE };
517+
if (vboard_equal(hashboard, &hash_entry->board)) { // (6%)
518+
hashmove[0] = hash_entry->data.move[0];
519+
lower = hash_entry->data.lower - ofssolid;
520+
upper = hash_entry->data.upper - ofssolid;
521+
// search_TC_NWS(&hash_data.data, search->eval.n_empties, NO_SELECTIVITY, alpha, &score)
522+
if (USE_TC /* && (data->wl.c.selectivity >= NO_SELECTIVITY && data->wl.c.depth >= search->eval.n_empties) */) {
523+
CUTOFF_STATS(++statistics.n_hash_try;)
524+
if (alpha < lower) {
525+
CUTOFF_STATS(++statistics.n_hash_high_cutoff;)
526+
return lower;
527+
}
528+
if (alpha >= upper) {
529+
CUTOFF_STATS(++statistics.n_hash_low_cutoff;)
530+
return upper;
531+
}
532+
}
518533
}
519-
// else if (ofssolid) // slows down
520-
// hash_get_from_board(&search->thread_hash, HBOARD_V(board0), &hash_data.data);
521-
522-
movelist_evaluate_fast(&movelist, search, &hash_data.data);
534+
if (movelist.n_moves > 1)
535+
movelist_evaluate_fast(&movelist, search, hashmove);
523536

524-
nodes_org = search->n_nodes;
537+
// nodes_org = search->n_nodes;
525538
parity0 = search->eval.parity;
526539
bestscore = -SCORE_INF;
540+
--search->eval.n_empties; // for next move
527541
// loop over all moves
528542
move = &movelist.move[0];
529-
if (--search->eval.n_empties <= DEPTH_TO_SHALLOW_SEARCH) // for next move (44%)
530-
while ((move = move_next_best(move))) { // (72%)
531-
search->eval.parity = parity0 ^ QUADRANT_ID[move->x];
543+
while ((move = move_next_best(move))) { // (76%)
544+
search->eval.parity = parity0 ^ QUADRANT_ID[move->x];
545+
vboard_update(&search->board, board0, move);
546+
if (search->eval.n_empties <= DEPTH_TO_SHALLOW_SEARCH) {
532547
search->empties[search->empties[move->x].previous].next = search->empties[move->x].next; // remove - maintain single link only
533-
vboard_update(&search->board, board0, move);
534548
score = -search_shallow(search, ~alpha, false);
535549
search->empties[search->empties[move->x].previous].next = move->x; // restore
536-
search->board = board0.board;
537-
538-
if (score > bestscore) { // (63%)
539-
bestscore = score;
540-
hash_data.data.move[0] = move->x;
541-
if (bestscore > alpha) break; // (48%)
542-
}
543-
}
544-
else
545-
while ((move = move_next_best(move))) { // (76%)
546-
search->eval.parity = parity0 ^ QUADRANT_ID[move->x];
550+
} else {
547551
empty_remove(search->empties, move->x);
548-
vboard_update(&search->board, board0, move);
549552
score = -NWS_endgame_local(search, ~alpha);
550553
empty_restore(search->empties, move->x);
551-
search->board = board0.board;
554+
}
555+
search->board = board0.board;
552556

553-
if (score > bestscore) { // (63%)
554-
bestscore = score;
555-
hash_data.data.move[0] = move->x;
556-
if (bestscore > alpha) break; // (39%)
557-
}
557+
if (score > bestscore) { // (63%)
558+
bestscore = score;
559+
bestmove = move->x;
560+
if (bestscore > alpha) break; // (39%)
558561
}
562+
}
559563
++search->eval.n_empties;
560564
search->eval.parity = parity0;
561565

562566
if (search->stop) // (1%)
563567
return alpha;
564568

565-
hash_data.data.wl.c.depth = search->eval.n_empties;
566-
hash_data.data.wl.c.selectivity = NO_SELECTIVITY;
567-
// hash_data.data.wl.c.cost = last_bit(search->n_nodes - nodes_org);
568-
// hash_data.data.move[0] = bestmove;
569-
hash_data.alpha = alpha + ofssolid;
570-
hash_data.beta = alpha + ofssolid + 1;
571-
hash_data.score = bestscore + ofssolid;
572-
hash_store_local(&search->thread_hash, &hashboard, hash_code, &hash_data);
573-
574-
// special cases
575-
} else if (movelist.n_moves == 1) { // (3%)
576-
parity0 = search->eval.parity;
577-
move = movelist_first(&movelist);
578-
search_swap_parity(search, move->x);
579-
empty_remove(search->empties, move->x);
580-
vboard_update(&search->board, board0, move);
581-
if (--search->eval.n_empties <= DEPTH_TO_SHALLOW_SEARCH) // (56%)
582-
bestscore = -search_shallow(search, ~alpha, false);
583-
else bestscore = -NWS_endgame_local(search, ~alpha);
584-
++search->eval.n_empties;
585-
empty_restore(search->empties, move->x);
586-
search->eval.parity = parity0;
587-
search->board = board0.board;
569+
hash_store_local(hash_entry, hashboard, alpha + ofssolid, alpha + ofssolid + 1, bestscore + ofssolid, bestmove);
588570

589571
} else { // (1%)
590572
if (can_move(search->board.opponent, search->board.player)) { // pass
@@ -608,7 +590,7 @@ static int NWS_endgame_local(Search *search, const int alpha)
608590
}
609591

610592
/**
611-
* @brief Evaluate an endgame position with a Null Window Search algorithm. (10..15 empties)
593+
* @brief Evaluate an endgame position with a Null Window Search algorithm. (11..15 empties)
612594
*
613595
* This function is used when there are still many empty squares on the board. Move
614596
* ordering, hash table cutoff, enhanced transposition cutoff, etc. are used in
@@ -628,46 +610,37 @@ int NWS_endgame(Search *search, const int alpha)
628610
Move *move;
629611
long long nodes_org;
630612
V2DI board0;
631-
Board hashboard;
632613
unsigned int parity0;
633-
unsigned long long full[5];
634614
MoveList movelist;
635615

636616
assert(bit_count(~(search->board.player|search->board.opponent)) < DEPTH_MIDGAME_TO_ENDGAME);
637617
assert(SCORE_MIN <= alpha && alpha <= SCORE_MAX);
638618

619+
if (search->stop) return alpha;
620+
639621
if (search->eval.n_empties <= DEPTH_TO_USE_LOCAL_HASH)
640622
return NWS_endgame_local(search, alpha);
641623

642-
if (search->stop) return alpha;
643-
644624
SEARCH_STATS(++statistics.n_NWS_endgame);
645625
SEARCH_UPDATE_INTERNAL_NODES(search->n_nodes);
646626

647627
// 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-
}
628+
if (search_SC_NWS(search, alpha, &score)) return score;
657629

658-
hash_code = board_get_hash_code(&hashboard);
630+
hash_code = board_get_hash_code(&search->board);
659631
hash_prefetch(&search->hash_table, hash_code);
660632

661633
search_get_movelist(search, &movelist);
634+
board0.board = search->board;
662635

663636
if (movelist.n_moves > 0) { // (96%)
664637
// transposition cutoff
665-
if (hash_get(&search->hash_table, &hashboard, hash_code, &hash_data.data)) { // (6%)
638+
if (hash_get(&search->hash_table, &search->board, hash_code, &hash_data.data)) { // (6%)
666639
if (search_TC_NWS(&hash_data.data, search->eval.n_empties, NO_SELECTIVITY, alpha, &score)) // (6%)
667640
return score;
668641
}
669642
if (movelist.n_moves > 1)
670-
movelist_evaluate_fast(&movelist, search, &hash_data.data);
643+
movelist_evaluate_fast(&movelist, search, hash_data.data.move);
671644

672645
nodes_org = search->n_nodes;
673646
parity0 = search->eval.parity;
@@ -702,7 +675,7 @@ int NWS_endgame(Search *search, const int alpha)
702675
hash_data.alpha = alpha;
703676
hash_data.beta = alpha + 1;
704677
hash_data.score = bestscore;
705-
hash_store(&search->hash_table, &hashboard, hash_code, &hash_data);
678+
hash_store(&search->hash_table, &search->board, hash_code, &hash_data);
706679

707680
} else { // (1%)
708681
if (can_move(search->board.opponent, search->board.player)) { // pass

0 commit comments

Comments
 (0)