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 }