Skip to content

Commit

Permalink
Implement PEXT bitboards for bishop and rook attacks (#451)
Browse files Browse the repository at this point in the history
  • Loading branch information
eduherminio authored Oct 20, 2023
1 parent 9f8f721 commit b4048f4
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 16 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ If you're a Linux user and are new to .NET ecosystem, the conversation in [this

- Razoring [[1](https://www.chessprogramming.org/Razoring)]

- PEXT Bitboards [[1](https://www.chessprogramming.org/BMI2#PEXTBitboards)] [[2](https://analog-hors.github.io/site/magic-bitboards/)]

## Credits

Lynx development wouldn't have been possible without:
Expand Down
8 changes: 4 additions & 4 deletions src/Lynx.Benchmark/InitializeBishopAndRookAttacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ public CustomPosition()
_pawnAttacks = AttackGenerator.InitializePawnAttacks();
_knightAttacks = AttackGenerator.InitializeKnightAttacks();

(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookAttacks();
(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopMagicAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookMagicAttacks();
}

/// <summary>
Expand All @@ -46,8 +46,8 @@ public CustomPosition(string _)
{
InitializePawnKnightAndKingAttacks();

(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookAttacks();
(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopMagicAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookMagicAttacks();
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Lynx.Benchmark/MakeUnmakeMove_implementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1261,8 +1261,8 @@ static MakeMoveAttacks()
PawnAttacks = AttackGenerator.InitializePawnAttacks();
KnightAttacks = AttackGenerator.InitializeKnightAttacks();

(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookAttacks();
(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopMagicAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookMagicAttacks();
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Lynx.Benchmark/MakeUnmakeMove_integration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1671,8 +1671,8 @@ static MakeMoveAttacks()
PawnAttacks = AttackGenerator.InitializePawnAttacks();
KnightAttacks = AttackGenerator.InitializeKnightAttacks();

(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookAttacks();
(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopMagicAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookMagicAttacks();
}

/// <summary>
Expand Down
84 changes: 84 additions & 0 deletions src/Lynx.Benchmark/PEXT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
*
* BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Ubuntu 22.04.3 LTS (Jammy Jellyfish)
* Intel Xeon Platinum 8171M CPU 2.60GHz, 1 CPU, 2 logical and 2 physical cores
* .NET SDK 8.0.100-rc.2.23502.2
* [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
* DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
*
*
* | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio |
* |------------- |---------:|--------:|--------:|------:|----------:|------------:|
* | MagicNumbers | 378.1 ns | 6.19 ns | 5.79 ns | 1.00 | - | NA |
* | PEXT | 229.7 ns | 2.79 ns | 2.61 ns | 0.61 | - | NA |
*
* BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 10 (10.0.20348.2031) (Hyper-V)
* Intel Xeon CPU E5-2673 v4 2.30GHz, 1 CPU, 2 logical and 2 physical cores
* .NET SDK 8.0.100-rc.2.23502.2
* [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
* DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
*
*
* | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
* |------------- |---------:|--------:|---------:|------:|--------:|----------:|------------:|
* | MagicNumbers | 408.9 ns | 8.14 ns | 13.59 ns | 1.00 | 0.00 | - | NA |
* | PEXT | 326.3 ns | 6.46 ns | 7.93 ns | 0.79 | 0.03 | - | NA |
*
* BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, macOS Monterey 12.6.9 (21G726) [Darwin 1.6.0]
* Intel Core i7-8700B CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 4 logical and 4 physical cores
* .NET SDK 8.0.100-rc.2.23502.2
* [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
* DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
*
*
* | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
* |------------- |---------:|---------:|---------:|------:|--------:|----------:|------------:|
* | MagicNumbers | 436.3 ns | 28.75 ns | 84.33 ns | 1.00 | 0.00 | - | NA |
* | PEXT | 274.5 ns | 20.23 ns | 58.69 ns | 0.66 | 0.19 | - | NA |
*
*/

using BenchmarkDotNet.Attributes;
using Lynx.Model;

namespace Lynx.Benchmark;
public class PEXTBenchmark : BaseBenchmark
{
private readonly Position _position = new Position(Constants.TrickyTestPositionFEN);

[Benchmark(Baseline = true)]
public ulong MagicNumbers()
{
ulong result = default;

for (int i = 0; i < 64; ++i)
{
result |= MagicNumbersRookAttacks(i, _position.OccupancyBitBoards[0]);
result |= MagicNumbersBishopAttacks(i, _position.OccupancyBitBoards[0]);
}

return result;
}

[Benchmark]
public ulong PEXT()
{
ulong result = default;

for (int i = 0; i < 64; ++i)
{
result |= PEXTRookAttacks(i, _position.OccupancyBitBoards[0]);
result |= PEXTBishopAttacks(i, _position.OccupancyBitBoards[0]);
}

return result;
}

private static BitBoard MagicNumbersRookAttacks(int squareIndex, BitBoard occupancy) => Attacks.MagicNumbersRookAttacks(squareIndex, occupancy);

private static BitBoard PEXTRookAttacks(int squareIndex, BitBoard occupancy) => Attacks.RookAttacks(squareIndex, occupancy);

private static BitBoard MagicNumbersBishopAttacks(int squareIndex, BitBoard occupancy) => Attacks.MagicNumbersBishopAttacks(squareIndex, occupancy);

private static BitBoard PEXTBishopAttacks(int squareIndex, BitBoard occupancy) => Attacks.BishopAttacks(squareIndex, occupancy);
}
4 changes: 2 additions & 2 deletions src/Lynx/AttackGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static BitBoard[] InitializeRookOccupancy()
/// Returns bishop occupancy masks and attacks
/// </summary>
/// <returns>(BitBoard[64], BitBoard[64, 512])</returns>
public static (BitBoard[] BishopOccupancyMasks, BitBoard[,] BishopAttacks) InitializeBishopAttacks()
public static (BitBoard[] BishopOccupancyMasks, BitBoard[,] BishopAttacks) InitializeBishopMagicAttacks()
{
BitBoard[] occupancyMasks = new BitBoard[64];
BitBoard[,] attacks = new BitBoard[64, 512];
Expand Down Expand Up @@ -102,7 +102,7 @@ public static (BitBoard[] BishopOccupancyMasks, BitBoard[,] BishopAttacks) Initi
/// Returns rook occupancy masks and attacks
/// </summary>
/// <returns>(BitBoard[64], BitBoard[64, 512])</returns>
public static (BitBoard[] RookOccupancyMasks, BitBoard[,] RookAttacks) InitializeRookAttacks()
public static (BitBoard[] RookOccupancyMasks, BitBoard[,] RookAttacks) InitializeRookMagicAttacks()
{
BitBoard[] occupancyMasks = new BitBoard[64];
BitBoard[,] attacks = new BitBoard[64, 4096];
Expand Down
70 changes: 67 additions & 3 deletions src/Lynx/Attacks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Lynx.Model;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics.X86;

namespace Lynx;

Expand All @@ -20,6 +22,10 @@ public static class Attacks
/// </summary>
private static readonly BitBoard[,] _rookAttacks;

private static readonly ulong[] _pextAttacks = new ulong[5248 + 102400];
private static readonly ulong[] _pextBishopOffset = new ulong[64];
private static readonly ulong[] _pextRookOffset = new ulong[64];

/// <summary>
/// [2 (B|W), 64 (Squares)]
/// </summary>
Expand All @@ -33,8 +39,21 @@ static Attacks()
PawnAttacks = AttackGenerator.InitializePawnAttacks();
KnightAttacks = AttackGenerator.InitializeKnightAttacks();

(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookAttacks();
(_bishopOccupancyMasks, _bishopAttacks) = AttackGenerator.InitializeBishopMagicAttacks();
(_rookOccupancyMasks, _rookAttacks) = AttackGenerator.InitializeRookMagicAttacks();

if (Bmi2.X64.IsSupported)
{
InitializeBishopAndRookPextAttacks();
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BitBoard BishopAttacks(int squareIndex, BitBoard occupancy)
{
return Bmi2.X64.IsSupported
? _pextAttacks[_pextBishopOffset[squareIndex] + Bmi2.X64.ParallelBitExtract(occupancy, _bishopOccupancyMasks[squareIndex])]
: MagicNumbersBishopAttacks(squareIndex, occupancy);
}

/// <summary>
Expand All @@ -44,7 +63,7 @@ static Attacks()
/// <param name="occupancy">Occupancy of <see cref="Side.Both"/></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BitBoard BishopAttacks(int squareIndex, BitBoard occupancy)
public static BitBoard MagicNumbersBishopAttacks(int squareIndex, BitBoard occupancy)
{
var occ = occupancy & _bishopOccupancyMasks[squareIndex];
occ *= Constants.BishopMagicNumbers[squareIndex];
Expand All @@ -61,6 +80,13 @@ public static BitBoard BishopAttacks(int squareIndex, BitBoard occupancy)
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BitBoard RookAttacks(int squareIndex, BitBoard occupancy)
{
return Bmi2.IsSupported
? _pextAttacks[_pextRookOffset[squareIndex] + Bmi2.X64.ParallelBitExtract(occupancy, _rookOccupancyMasks[squareIndex])]
: MagicNumbersRookAttacks(squareIndex, occupancy);
}

public static BitBoard MagicNumbersRookAttacks(int squareIndex, BitBoard occupancy)
{
var occ = occupancy & _rookOccupancyMasks[squareIndex];
occ *= Constants.RookMagicNumbers[squareIndex];
Expand Down Expand Up @@ -175,4 +201,42 @@ private static bool IsSquareAttackedByQueens(int offset, BitBoard bishopAttacks,
var queenAttacks = QueenAttacks(rookAttacks, bishopAttacks);
return (queenAttacks & piecePosition[(int)Piece.Q + offset]) != default;
}

/// <summary>
/// Taken from Leorik (https://github.com/lithander/Leorik/blob/master/Leorik.Core/Slider/Pext.cs)
/// Based on https://www.chessprogramming.org/BMI2#PEXT_Bitboards
/// </summary>
private static void InitializeBishopAndRookPextAttacks()
{
ulong index = 0;

// Bishop-Attacks
for (int square = 0; square < 64; square++)
{
_pextBishopOffset[square] = index;
ulong bishopMask = _bishopOccupancyMasks[square];

ulong patterns = 1UL << BitOperations.PopCount(bishopMask);

for (ulong i = 0; i < patterns; i++)
{
ulong occupation = Bmi2.X64.ParallelBitDeposit(i, bishopMask);
_pextAttacks[index++] = MagicNumbersBishopAttacks(square, occupation);
}
}

// Rook-Attacks
for (int square = 0; square < 64; square++)
{
_pextRookOffset[square] = index;
ulong rookMask = _rookOccupancyMasks[square];
ulong patterns = 1UL << BitOperations.PopCount(rookMask);

for (ulong i = 0; i < patterns; i++)
{
ulong occupation = Bmi2.X64.ParallelBitDeposit(i, rookMask);
_pextAttacks[index++] = MagicNumbersRookAttacks(square, occupation);
}
}
}
}
20 changes: 20 additions & 0 deletions src/Lynx/LynxDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Lynx.UCI.Commands.Engine;
using Lynx.UCI.Commands.GUI;
using NLog;
using System.Runtime.Intrinsics.X86;
using System.Text.Json;
using System.Threading.Channels;

Expand Down Expand Up @@ -100,6 +101,9 @@ static ReadOnlySpan<char> ExtractCommandItems(string rawCommand)
case "printsettings":
await HandleSettings();
break;
case "printsysteminfo":
await HandleSystemInfo();
break;
case "staticeval":
await HandleStaticEval(rawCommand, cancellationToken);
HandleQuit();
Expand Down Expand Up @@ -342,6 +346,22 @@ private async ValueTask HandleSettings()
await _engineWriter.Writer.WriteAsync(message);
}

private async ValueTask HandleSystemInfo()
{
try
{
var simd = Bmi2.X64.IsSupported
? "Bmi2.X64 supported, PEXT BitBoards will be used"
: "Bmi2.X64 not supported";

await _engineWriter.Writer.WriteAsync(simd);
}
catch (Exception e)
{
_logger.Error(e);
}
}

private async ValueTask HandleStaticEval(string rawCommand, CancellationToken cancellationToken)
{
try
Expand Down
3 changes: 2 additions & 1 deletion tests/Lynx.Test/PregeneratedAttacks/BishopAttacksTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ static void ValidateAttacks(BS[] attackedSquares, BitBoard attacks)
}

/// <summary>
/// Implicitly tests <see cref="AttackGenerator.InitializeBishopAttacks"/> and <see cref="Constants.BishopMagicNumbers"/>
/// Implicitly tests <see cref="AttackGenerator.InitializeBishopMagicAttacks"/> and <see cref="Constants.BishopMagicNumbers"/>
/// </summary>
/// <param name="bishopSquare"></param>
/// <param name="occupiedSquares"></param>
Expand Down Expand Up @@ -76,6 +76,7 @@ public void GetBishopAttacks(BS bishopSquare, BS[] occupiedSquares, BS[] attacke

// Act
var attacks = Attacks.BishopAttacks((int)bishopSquare, occupancy);
Assert.AreEqual(Attacks.MagicNumbersBishopAttacks((int)bishopSquare, occupancy), attacks);

// Assert
foreach (var attackedSquare in attackedSquares)
Expand Down
2 changes: 1 addition & 1 deletion tests/Lynx.Test/PregeneratedAttacks/QueenAttacksTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Lynx.Test.PregeneratedAttacks;
public class QueenAttacksTest
{
/// <summary>
/// Implicitly tests <see cref="AttackGenerator.InitializeBishopAttacks"/> and <see cref="Constants.BishopMagicNumbers"/>
/// Implicitly tests <see cref="AttackGenerator.InitializeBishopMagicAttacks"/> and <see cref="Constants.BishopMagicNumbers"/>
/// </summary>
/// <param name="bishopSquare"></param>
/// <param name="occupiedSquares"></param>
Expand Down
3 changes: 2 additions & 1 deletion tests/Lynx.Test/PregeneratedAttacks/RookAttacksTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void GenerateRookAttacksOnTheFly(BS rookSquare, BS[] occupiedSquares, BS[
}

/// <summary>
/// Implicitly tests <see cref="AttackGenerator.InitializeRookAttacks"/> and <see cref="Constants.RookMagicNumbers"/>
/// Implicitly tests <see cref="AttackGenerator.InitializeRookMagicAttacks"/> and <see cref="Constants.RookMagicNumbers"/>
/// </summary>
/// <param name="rookSquare"></param>
/// <param name="occupiedSquares"></param>
Expand Down Expand Up @@ -71,6 +71,7 @@ public void GetRookAttacks(BS rookSquare, BS[] occupiedSquares, BS[] attackedSqu

// Act
var attacks = Attacks.RookAttacks((int)rookSquare, occupancy);
Assert.AreEqual(Attacks.MagicNumbersRookAttacks((int)rookSquare, occupancy), attacks);

// Assert
ValidateAttacks(attackedSquares, attacks);
Expand Down

0 comments on commit b4048f4

Please sign in to comment.