github.com/frankkopp/FrankyGo@v1.0.3/internal/transpositiontable/tt.go (about)

     1  //
     2  // FrankyGo - UCI chess engine in GO for learning purposes
     3  //
     4  // MIT License
     5  //
     6  // Copyright (c) 2018-2020 Frank Kopp
     7  //
     8  // Permission is hereby granted, free of charge, to any person obtaining a copy
     9  // of this software and associated documentation files (the "Software"), to deal
    10  // in the Software without restriction, including without limitation the rights
    11  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    12  // copies of the Software, and to permit persons to whom the Software is
    13  // furnished to do so, subject to the following conditions:
    14  //
    15  // The above copyright notice and this permission notice shall be included in all
    16  // copies or substantial portions of the Software.
    17  //
    18  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    19  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    20  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    21  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    22  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    23  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    24  // SOFTWARE.
    25  //
    26  
    27  // Package transpositiontable implements a transposition table (cache)
    28  // data structure and functionality for a chess engine search.
    29  // The TtTable class is not thread safe and needs to be synchronized
    30  // externally if used from multiple threads. Is especially relevant
    31  // for Resize and Clear which should not be called in parallel
    32  // while searching.
    33  package transpositiontable
    34  
    35  import (
    36  	"math"
    37  	"sync"
    38  	"time"
    39  	"unsafe"
    40  
    41  	"github.com/op/go-logging"
    42  	"golang.org/x/text/language"
    43  	"golang.org/x/text/message"
    44  
    45  	myLogging "github.com/frankkopp/FrankyGo/internal/logging"
    46  	"github.com/frankkopp/FrankyGo/internal/position"
    47  	. "github.com/frankkopp/FrankyGo/internal/types"
    48  	"github.com/frankkopp/FrankyGo/internal/util"
    49  )
    50  
    51  var out = message.NewPrinter(language.German)
    52  
    53  // TtEntry struct is the data structure for each entry in the transposition
    54  // table. Each entry has 16-bytes (128-bits)
    55  type TtEntry struct {
    56  	Key        position.Key // 64-bit Zobrist Key
    57  	Move       Move         // 32-bit Move and value
    58  	Depth      int8         // 7-bit 0-127 0b01111111
    59  	Age        int8         // 3-bit 0-7   0b00000111 0=used 1=generated, not used, >1 older generation
    60  	Type       ValueType    // 2-bit None, Exact, Alpha (upper), Beta (lower)
    61  	MateThreat bool         // 1-bit
    62  }
    63  
    64  const (
    65  	// TtEntrySize is the size in bytes for each TtEntry
    66  	TtEntrySize = 16 // 16 bytes
    67  
    68  	// MaxSizeInMB maximal memory usage of tt
    69  	MaxSizeInMB = 65_536
    70  )
    71  
    72  // TtTable is the actual transposition table
    73  // object holding data and state.
    74  // Create with NewTtTable()
    75  type TtTable struct {
    76  	log                *logging.Logger
    77  	data               []TtEntry
    78  	sizeInByte         uint64
    79  	hashKeyMask        uint64
    80  	maxNumberOfEntries uint64
    81  	numberOfEntries    uint64
    82  	Stats              TtStats
    83  }
    84  
    85  // TtStats holds statistical data on tt usage
    86  type TtStats struct {
    87  	numberOfPuts       uint64
    88  	numberOfCollisions uint64
    89  	numberOfOverwrites uint64
    90  	numberOfUpdates    uint64
    91  	numberOfProbes     uint64
    92  	numberOfHits       uint64
    93  	numberOfMisses     uint64
    94  }
    95  
    96  // NewTtTable creates a new TtTable with the given number of bytes
    97  // as a maximum of memory usage. actual size will be determined
    98  // by the number of elements fitting into this size which need
    99  // to be a power of 2 for efficient hashing/addressing via bit
   100  // masks
   101  func NewTtTable(sizeInMByte int) *TtTable {
   102  	tt := TtTable{
   103  		log:                myLogging.GetLog(),
   104  		data:               nil,
   105  		sizeInByte:         0,
   106  		hashKeyMask:        0,
   107  		maxNumberOfEntries: 0,
   108  		numberOfEntries:    0,
   109  	}
   110  	tt.Resize(sizeInMByte)
   111  	return &tt
   112  }
   113  
   114  // Resize resizes the tt table. All entries will be cleared.
   115  // The TtTable class is not thread safe and needs to be synchronized
   116  // externally if used from multiple threads. Is especially relevant
   117  // for Resize and Clear which should not be called in parallel
   118  // while searching.
   119  func (tt *TtTable) Resize(sizeInMByte int) {
   120  	if sizeInMByte > MaxSizeInMB {
   121  		tt.log.Error(out.Sprintf("Requested size for TT of %d MB reduced to max of %d MB", sizeInMByte, MaxSizeInMB))
   122  		sizeInMByte = MaxSizeInMB
   123  	}
   124  
   125  	// calculate the maximum power of 2 of entries fitting into the given size in MB
   126  	tt.sizeInByte = uint64(sizeInMByte) * MB
   127  	tt.maxNumberOfEntries = 1 << uint64(math.Floor(math.Log2(float64(tt.sizeInByte/TtEntrySize))))
   128  	tt.hashKeyMask = tt.maxNumberOfEntries - 1 // --> 0x0001111....111
   129  
   130  	// if TT is resized to 0 we cant have any entries.
   131  	if tt.sizeInByte == 0 {
   132  		tt.maxNumberOfEntries = 0
   133  	}
   134  
   135  	// calculate the real memory usage
   136  	tt.sizeInByte = tt.maxNumberOfEntries * TtEntrySize
   137  
   138  	// Create new slice/array - garbage collections takes care of cleanup
   139  	tt.data = make([]TtEntry, tt.maxNumberOfEntries, tt.maxNumberOfEntries)
   140  
   141  	tt.log.Info(out.Sprintf("TT Size %d MByte, Capacity %d entries (size=%dByte) (Requested were %d MBytes)",
   142  		tt.sizeInByte/MB, tt.maxNumberOfEntries, unsafe.Sizeof(TtEntry{}), sizeInMByte))
   143  	tt.log.Debug(util.MemStat())
   144  }
   145  
   146  // GetEntry returns a pointer to the corresponding tt entry.
   147  // Given key is checked against the entry's key. When
   148  // equal pointer to entry will be returned. Otherwise
   149  // nil will be returned.
   150  // Does not change statistics.
   151  func (tt *TtTable) GetEntry(key position.Key) *TtEntry {
   152  	e := &tt.data[tt.hash(key)]
   153  	if e.Key == key {
   154  		return e
   155  	}
   156  	return nil
   157  }
   158  
   159  // Probe returns a pointer to the corresponding tt entry
   160  // or nil if it was not found. Decreases TtEntry.Age by 1
   161  func (tt *TtTable) Probe(key position.Key) *TtEntry {
   162  	tt.Stats.numberOfProbes++
   163  	e := &tt.data[tt.hash(key)]
   164  	if e.Key == key {
   165  		e.Age--
   166  		if e.Age < 0 {
   167  			e.Age = 0
   168  		}
   169  		tt.Stats.numberOfHits++
   170  		return e
   171  	}
   172  	tt.Stats.numberOfMisses++
   173  	return nil
   174  }
   175  
   176  // Put an TtEntry into the tt. Encodes value into the move.
   177  func (tt *TtTable) Put(key position.Key, move Move, depth int8, value Value, valueType ValueType, mateThreat bool) {
   178  
   179  	// if the size of the TT = 0 we
   180  	// do not store anything
   181  	if tt.maxNumberOfEntries == 0 {
   182  		return
   183  	}
   184  
   185  	// read the entries for this hash
   186  	entryDataPtr := &tt.data[tt.hash(key)]
   187  	// encode value into the move if it is a valid value (min < v < max)
   188  	if value.IsValid() {
   189  		move = move.SetValue(value)
   190  	} else {
   191  		tt.log.Warningf("TT Put: Tried to store an invalid Value into the TT %s (%d)", value.String(), int(value))
   192  	}
   193  
   194  	tt.Stats.numberOfPuts++
   195  
   196  	// NewTtTable entry
   197  	if entryDataPtr.Key == 0 {
   198  		tt.numberOfEntries++
   199  		entryDataPtr.Key = key
   200  		entryDataPtr.Move = move
   201  		entryDataPtr.Depth = depth
   202  		entryDataPtr.Age = 1
   203  		entryDataPtr.Type = valueType
   204  		entryDataPtr.MateThreat = mateThreat
   205  		return
   206  	}
   207  
   208  	// Same hash but different position
   209  	if entryDataPtr.Key != key {
   210  		tt.Stats.numberOfCollisions++
   211  		// overwrite if
   212  		// - the new entry's depth is higher
   213  		// - the new entry's depth is same and the previous entry is old (is aged)
   214  		if depth > entryDataPtr.Depth ||
   215  			(depth == entryDataPtr.Depth && entryDataPtr.Age > 1) {
   216  			tt.Stats.numberOfOverwrites++
   217  			entryDataPtr.Key = key
   218  			entryDataPtr.Move = move
   219  			entryDataPtr.Depth = depth
   220  			entryDataPtr.Age = 1
   221  			entryDataPtr.Type = valueType
   222  			entryDataPtr.MateThreat = mateThreat
   223  		}
   224  		return
   225  	}
   226  
   227  	// Same hash and same position -> update entry?
   228  	if entryDataPtr.Key == key {
   229  		tt.Stats.numberOfUpdates++
   230  		// we always update as the stored moved can't be any good otherwise
   231  		// we would have found this during the search in a previous probe
   232  		// and we would not have come to store it again
   233  		entryDataPtr.Key = key
   234  		entryDataPtr.Move = move
   235  		entryDataPtr.Depth = depth
   236  		entryDataPtr.Age = 1
   237  		entryDataPtr.Type = valueType
   238  		entryDataPtr.MateThreat = mateThreat
   239  		return
   240  	}
   241  }
   242  
   243  // Clear clears all entries of the tt
   244  // The TtTable class is not thread safe and needs to be synchronized
   245  // externally if used from multiple threads. Is especially relevant
   246  // for Resize and Clear which should not be called in parallel
   247  // while searching.
   248  func (tt *TtTable) Clear() {
   249  	// Create new slice/array - garbage collections takes care of cleanup
   250  	tt.data = make([]TtEntry, tt.maxNumberOfEntries, tt.maxNumberOfEntries)
   251  	tt.numberOfEntries = 0
   252  	tt.Stats = TtStats{}
   253  }
   254  
   255  // Hashfull returns how full the transposition table is in permill as per UCI
   256  func (tt *TtTable) Hashfull() int {
   257  	if tt.maxNumberOfEntries == 0 {
   258  		return 0
   259  	}
   260  	return int((1000 * tt.numberOfEntries) / tt.maxNumberOfEntries)
   261  }
   262  
   263  // String returns a string representation of this TtTable instance
   264  func (tt *TtTable) String() string {
   265  	return out.Sprintf("TT: size %d MB max entries %d of size %d Bytes entries %d (%d%%) puts %d "+
   266  		"updates %d collisions %d overwrites %d probes %d hits %d (%d%%) misses %d (%d%%)",
   267  		tt.sizeInByte/MB, tt.maxNumberOfEntries, unsafe.Sizeof(TtEntry{}), tt.numberOfEntries, tt.Hashfull()/10,
   268  		tt.Stats.numberOfPuts, tt.Stats.numberOfUpdates, tt.Stats.numberOfCollisions, tt.Stats.numberOfOverwrites, tt.Stats.numberOfProbes,
   269  		tt.Stats.numberOfHits, (tt.Stats.numberOfHits*100)/(1+tt.Stats.numberOfProbes),
   270  		tt.Stats.numberOfMisses, (tt.Stats.numberOfMisses*100)/(1+tt.Stats.numberOfProbes))
   271  }
   272  
   273  // Len returns the number of non empty entries in the tt
   274  func (tt *TtTable) Len() uint64 {
   275  	return tt.numberOfEntries
   276  }
   277  
   278  
   279  // AgeEntries ages each entry in the tt
   280  // Creates a number of go routines with processes each
   281  // a certain slice of data to process
   282  func (tt *TtTable) AgeEntries() {
   283  	startTime := time.Now()
   284  	if tt.numberOfEntries > 0 {
   285  		numberOfGoroutines := uint64(32) // arbitrary - uses up to 32 threads
   286  		var wg sync.WaitGroup
   287  		wg.Add(int(numberOfGoroutines))
   288  		slice := tt.maxNumberOfEntries / numberOfGoroutines
   289  		for i := uint64(0); i < numberOfGoroutines; i++ {
   290  			go func(i uint64) {
   291  				defer wg.Done()
   292  				start := i * slice
   293  				end := start + slice
   294  				if i == numberOfGoroutines-1 {
   295  					end = tt.maxNumberOfEntries
   296  				}
   297  				for n := start; n < end; n++ {
   298  					if tt.data[n].Key != 0 {
   299  						tt.data[n].Age++
   300  					}
   301  				}
   302  			}(i)
   303  		}
   304  		wg.Wait()
   305  	}
   306  	elapsed := time.Since(startTime)
   307  	tt.log.Debug(out.Sprintf("Aged %d entries of %d in %d ms\n", tt.numberOfEntries, len(tt.data), elapsed.Milliseconds()))
   308  }
   309  
   310  // ///////////////////////////////////////////////////////////
   311  // Private
   312  // ///////////////////////////////////////////////////////////
   313  
   314  // hash generates the internal hash key for the data array
   315  func (tt *TtTable) hash(key position.Key) uint64 {
   316  	return uint64(key) & tt.hashKeyMask
   317  }