diff --git a/src/Lynx/Model/TranspositionTable.cs b/src/Lynx/Model/TranspositionTable.cs index 3a8531222..d243b9ba7 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -111,24 +111,32 @@ public readonly ulong CalculateTTIndex(ulong positionUniqueIdentifier, int halfM public readonly bool ProbeHash(Position position, int halfMovesWithoutCaptureOrPawnMove, int ply, out TTProbeResult result) // [MaybeNullWhen(false)] { var ttIndex = CalculateTTIndex(position.UniqueIdentifier, halfMovesWithoutCaptureOrPawnMove); - var bucket = _tt[ttIndex]; - var key = GenerateTTKey(position.UniqueIdentifier); - - // We simply take the first entry - for (int i = 0; i < Constants.TranspositionTableElementsPerBucket; ++i) + unsafe { - ref var entry = ref bucket[i]; - - if (key == entry.Key) + fixed (TranspositionTableBucket* ttPtr = _tt) { - // We want to translate the checkmate position relative to the saved node to our root position from which we're searching - // If the recorded score is a checkmate in 3 and we are at depth 5, we want to read checkmate in 8 - var recalculatedScore = RecalculateMateScores(entry.Score, ply); + var bucketPtr = ttPtr + ttIndex; + var bucket = (TranspositionTableElement*)bucketPtr; + + var key = GenerateTTKey(position.UniqueIdentifier); + + // We simply take the first entry + for (int i = 0; i < Constants.TranspositionTableElementsPerBucket; ++i) + { + ref var entry = ref bucket[i]; + + if (key == entry.Key) + { + // We want to translate the checkmate position relative to the saved node to our root position from which we're searching + // If the recorded score is a checkmate in 3 and we are at depth 5, we want to read checkmate in 8 + var recalculatedScore = RecalculateMateScores(entry.Score, ply); - result = new TTProbeResult(recalculatedScore, entry.Move, entry.Type, entry.StaticEval, entry.Depth, entry.WasPv); + result = new TTProbeResult(recalculatedScore, entry.Move, entry.Type, entry.StaticEval, entry.Depth, entry.WasPv); - return true; + return true; + } + } } } @@ -146,102 +154,96 @@ public readonly void RecordHash(Position position, int halfMovesWithoutCaptureOr Debug.Assert(nodeType != NodeType.Alpha || move is null, "Assertion failed", "There's no 'best move' on fail-lows, so TT one won't be overriden"); var ttIndex = CalculateTTIndex(position.UniqueIdentifier, halfMovesWithoutCaptureOrPawnMove); - ref var bucket = ref _tt[ttIndex]; - ref TranspositionTableElement entry = ref bucket[0]; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static int CalculateBucketWeight(TranspositionTableElement entry, int ttAge) + unsafe { - // Another way of doing: - // var ageDiff = age - entry.Age - // if (ageDiff <0) ageDiff += maxAge - var relativeAge = (ttAge - entry.Age + TranspositionTableElement.MaxAge) & TranspositionTableElement.AgeMask; + fixed (TranspositionTableBucket* ttPtr = _tt) + { + var bucketPtr = ttPtr + ttIndex; + var bucket = (TranspositionTableElement*)bucketPtr; - var value = entry.Depth - (2 * relativeAge); - return value; - } + ref TranspositionTableElement entry = ref bucket[0]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int CalculateBucketWeight(TranspositionTableElement entry, int ttAge) + { + // Another way of doing: + // var ageDiff = age - entry.Age + // if (ageDiff <0) ageDiff += maxAge + var relativeAge = (ttAge - entry.Age + TranspositionTableElement.MaxAge) & TranspositionTableElement.AgeMask; + + var value = entry.Depth - (2 * relativeAge); + return value; + } #if DEBUG - int bucketIndex = 0; + int bucketIndex = 0; #endif - var newKey = GenerateTTKey(position.UniqueIdentifier); + var newKey = GenerateTTKey(position.UniqueIdentifier); - if (entry.Key != newKey && entry.Type != NodeType.Unknown) - { - int minValue = CalculateBucketWeight(entry, _age); + if (entry.Key != newKey && entry.Type != NodeType.Unknown) + { + int minValue = CalculateBucketWeight(entry, _age); - for (int i = 1; i < Constants.TranspositionTableElementsPerBucket; ++i) - { - ref var candidateEntry = ref bucket[i]; + for (int i = 1; i < Constants.TranspositionTableElementsPerBucket; ++i) + { + ref var candidateEntry = ref bucket[i]; - // Bucket policy to discard very old entries + // Bucket policy to discard very old entries - // Always take an empty entry, or one that corresponds to the same position - if (candidateEntry.Type == NodeType.Unknown || candidateEntry.Key == newKey) - { - entry = ref candidateEntry; + // Always take an empty entry, or one that corresponds to the same position + if (candidateEntry.Type == NodeType.Unknown || candidateEntry.Key == newKey) + { + entry = ref candidateEntry; #if DEBUG - bucketIndex = i; + bucketIndex = i; #endif - break; - } + break; + } - // Otherwise, take the entry with the lowest weight (calculated based on depth and age) - // Current formula from Stormphrax - var value = CalculateBucketWeight(candidateEntry, _age); + // Otherwise, take the entry with the lowest weight (calculated based on depth and age) + // Current formula from Stormphrax + var value = CalculateBucketWeight(candidateEntry, _age); - if (value < minValue) - { - minValue = value; - entry = ref candidateEntry; + if (value < minValue) + { + minValue = value; + entry = ref candidateEntry; #if DEBUG - bucketIndex = i; + bucketIndex = i; #endif + } + } } - } - } - var wasPvInt = wasPv ? 1 : 0; + var wasPvInt = wasPv ? 1 : 0; - // Replacement policy - bool shouldReplace = - entry.Key != newKey // Different key: collision or no actual entry - || nodeType == NodeType.Exact // Entering PV data - || entry.Age != _age // Different age/generation - || depth // Higher depth - + Configuration.EngineSettings.TTReplacement_DepthOffset - + (Configuration.EngineSettings.TTReplacement_TTPVDepthOffset * wasPvInt) - >= entry.Depth; - - if (!shouldReplace) - { - return; - } + // Replacement policy + bool shouldReplace = + entry.Key != newKey // Different key: collision or no actual entry + || nodeType == NodeType.Exact // Entering PV data + || entry.Age != _age // Different age/generation + || depth // Higher depth + + Configuration.EngineSettings.TTReplacement_DepthOffset + + (Configuration.EngineSettings.TTReplacement_TTPVDepthOffset * wasPvInt) + >= entry.Depth; - // We want to store the distance to the checkmate position relative to the current node, independently from the root - // If the evaluated score is a checkmate in 8 and we're at depth 5, we want to store checkmate value in 3 - var recalculatedScore = RecalculateMateScores(score, -ply); + if (!shouldReplace) + { + return; + } - entry.Update(newKey, recalculatedScore, staticEval, depth, nodeType, wasPvInt, move, _age); + // We want to store the distance to the checkmate position relative to the current node, independently from the root + // If the evaluated score is a checkmate in 8 and we're at depth 5, we want to store checkmate value in 3 + var recalculatedScore = RecalculateMateScores(score, -ply); -#if DEBUG - Debug.Assert(bucket[bucketIndex].Score == recalculatedScore); - Debug.Assert(bucket[bucketIndex].Type == nodeType); - Debug.Assert(bucket[bucketIndex].Age == _age); - Debug.Assert(_tt[ttIndex][bucketIndex].Score == recalculatedScore); - Debug.Assert(_tt[ttIndex][bucketIndex].Type == nodeType); - Debug.Assert(bucket[bucketIndex].Age == _age); - - if (_tt[ttIndex][bucketIndex].Score != recalculatedScore) - { - throw new LynxException(); + entry.Update(newKey, recalculatedScore, staticEval, depth, nodeType, wasPvInt, move, _age); + } } -#endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -280,12 +282,21 @@ public readonly int HashfullPermillApprox() { for (int i = 0; i < 1_000; ++i) { - var bucket = _tt[i]; - for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + unsafe { - if (bucket[j].Key != default) + fixed (TranspositionTableBucket* ttPtr = _tt) { - ++items; + var bucketPtr = ttPtr + i; + var bucket = (TranspositionTableElement*)bucketPtr; + + for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + { + TranspositionTableElement entry = bucket[j]; + if (entry.Key != default) + { + ++items; + } + } } } } @@ -344,12 +355,21 @@ private readonly int PopulatedItemsCount() int items = 0; for (int i = 0; i < _tt.Length; ++i) { - var bucket = _tt[i]; - for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + unsafe { - if (bucket[j].Key != default) + fixed (TranspositionTableBucket* ttPtr = _tt) { - ++items; + var bucketPtr = ttPtr + i; + var bucket = (TranspositionTableElement*)bucketPtr; + + for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + { + TranspositionTableElement entry = bucket[j]; + if (entry.Key != default) + { + ++items; + } + } } } } @@ -366,12 +386,21 @@ private readonly void Stats() int items = 0; for (int i = 0; i < _tt.Length; ++i) { - var bucket = _tt[i]; - for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + unsafe { - if (bucket[j].Key != default) + fixed (TranspositionTableBucket* ttPtr = _tt) { - ++items; + var bucketPtr = ttPtr + i; + var bucket = (TranspositionTableElement*)bucketPtr; + + for (int j = 0; j < Constants.TranspositionTableElementsPerBucket; ++j) + { + TranspositionTableElement entry = bucket[j]; + if (entry.Key != default) + { + ++items; + } + } } } } diff --git a/src/Lynx/Model/TranspositionTableBucket.cs b/src/Lynx/Model/TranspositionTableBucket.cs index 26dd12b04..2556c4242 100644 --- a/src/Lynx/Model/TranspositionTableBucket.cs +++ b/src/Lynx/Model/TranspositionTableBucket.cs @@ -3,11 +3,21 @@ namespace Lynx.Model; -[InlineArray(Constants.TranspositionTableElementsPerBucket)] +/// +/// A pointer to this (TranspositionTableBucket*) can be casted to a TranspositionTableElement* and indexed from 0 to - 1 to access each entry. +/// +[StructLayout(LayoutKind.Explicit, Size = 30)] public struct TranspositionTableBucket { #pragma warning disable S1144, RCS1213 // Unused private types or members should be removed - private TranspositionTableElement _ttEntry; + [FieldOffset(0)] + private TranspositionTableElement _ttEntry0; + + [FieldOffset(10)] + private TranspositionTableElement _ttEntry1; + + [FieldOffset(20)] + private TranspositionTableElement _ttEntry2; #pragma warning restore S1144 // Unused private types or members should be removed // TODO Add byte padding to align with 32 or 64 bytes