diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..8981efcaf13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Files from build +**/*.o +**/*.s +src/.depend + +# Built binary +src/stockfish* +src/-lstdc++.res + +# Neural network for the NNUE evaluation +**/*.nnue + diff --git a/Readme.md b/Readme.md index a759eff665b..330d19edd1f 100644 --- a/Readme.md +++ b/Readme.md @@ -1,48 +1,55 @@ ## Overview -[![Build Status](https://travis-ci.org/official-stockfish/Stockfish.svg?branch=master)](https://travis-ci.org/official-stockfish/Stockfish) +[![Build Status](https://github.com/official-stockfish/Stockfish/actions/workflows/stockfish.yml/badge.svg)](https://github.com/official-stockfish/Stockfish/actions) [![Build Status](https://ci.appveyor.com/api/projects/status/github/official-stockfish/Stockfish?branch=master&svg=true)](https://ci.appveyor.com/project/mcostalba/stockfish/branch/master) [Stockfish](https://stockfishchess.org) is a free, powerful UCI chess engine -derived from Glaurung 2.1. It is not a complete chess program and requires a -UCI-compatible GUI (e.g. XBoard with PolyGlot, Scid, Cute Chess, eboard, Arena, -Sigma Chess, Shredder, Chess Partner or Fritz) in order to be used comfortably. -Read the documentation for your GUI of choice for information about how to use -Stockfish with it. +derived from Glaurung 2.1. Stockfish is not a complete chess program and requires a +UCI-compatible graphical user interface (GUI) (e.g. XBoard with PolyGlot, Scid, +Cute Chess, eboard, Arena, Sigma Chess, Shredder, Chess Partner or Fritz) in order +to be used comfortably. Read the documentation for your GUI of choice for information +about how to use Stockfish with it. + +The Stockfish engine features two evaluation functions for chess, the classical +evaluation based on handcrafted terms, and the NNUE evaluation based on efficiently +updatable neural networks. The classical evaluation runs efficiently on almost all +CPU architectures, while the NNUE evaluation benefits from the vector +intrinsics available on most CPUs (sse2, avx2, neon, or similar). ## Files This distribution of Stockfish consists of the following files: - * Readme.md, the file you are currently reading. - - * Copying.txt, a text file containing the GNU General Public License version 3. + * [Readme.md](https://github.com/official-stockfish/Stockfish/blob/master/README.md), the file you are currently reading. - * src, a subdirectory containing the full source code, including a Makefile - that can be used to compile Stockfish on Unix-like systems. + * [Copying.txt](https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt), a text file containing the GNU General Public License version 3. + * [AUTHORS](https://github.com/official-stockfish/Stockfish/blob/master/AUTHORS), a text file with the list of authors for the project -## UCI parameters + * [src](https://github.com/official-stockfish/Stockfish/tree/master/src), a subdirectory containing the full source code, including a Makefile + that can be used to compile Stockfish on Unix-like systems. -Currently, Stockfish has the following UCI options: + * a file with the .nnue extension, storing the neural network for the NNUE + evaluation. Binary distributions will have this file embedded. - * #### Debug Log File - Write all communication to and from the engine into a text file. +## The UCI protocol and available options - * #### Contempt - A positive value for contempt favors middle game positions and avoids draws. +The Universal Chess Interface (UCI) is a standard protocol used to communicate with +a chess engine, and is the recommended way to do so for typical graphical user interfaces +(GUI) or chess tools. Stockfish implements the majority of it options as described +in [the UCI protocol](https://www.shredderchess.com/download/div/uci.zip). - * #### Analysis Contempt - By default, contempt is set to prefer the side to move. Set this option to "White" - or "Black" to analyse with contempt for that side, or "Off" to disable contempt. +Developers can see the default values for UCI options available in Stockfish by typing +`./stockfish uci` in a terminal, but the majority of users will typically see them and +change them via a chess GUI. This is a list of available UCI options in Stockfish: * #### Threads The number of CPU threads used for searching a position. For best performance, set this equal to the number of CPU cores available. * #### Hash - The size of the hash table in MB. + The size of the hash table in MB. It is recommended to set Hash after setting Threads. * #### Clear Hash Clear the hash table. @@ -54,10 +61,27 @@ Currently, Stockfish has the following UCI options: Output the N best lines (principal variations, PVs) when searching. Leave at 1 for best performance. - * #### Skill Level - Lower the Skill Level in order to make Stockfish play weaker (see also UCI_LimitStrength). - Internally, MultiPV is enabled, and with a certain probability depending on the Skill Level a - weaker move will be played. + * #### Use NNUE + Toggle between the NNUE and classical evaluation functions. If set to "true", + the network parameters must be available to load from file (see also EvalFile), + if they are not embedded in the binary. + + * #### EvalFile + The name of the file of the NNUE evaluation parameters. Depending on the GUI the + filename might have to include the full path to the folder/directory that contains the file. + Other locations, such as the directory that contains the binary and the working directory, + are also searched. + + * #### UCI_AnalyseMode + An option handled by your GUI. + + * #### UCI_Chess960 + An option handled by your GUI. If true, Stockfish will play Chess960. + + * #### UCI_ShowWDL + If enabled, show approximate WDL statistics as part of the engine output. + These WDL numbers model expected game outcomes for a given evaluation and + game ply for engine self-play at fishtest LTC conditions (60+0.6s per game). * #### UCI_LimitStrength Enable weaker play aiming for an Elo rating as set by UCI_Elo. This option overrides Skill Level. @@ -66,26 +90,10 @@ Currently, Stockfish has the following UCI options: If enabled by UCI_LimitStrength, aim for an engine strength of the given Elo. This Elo rating has been calibrated at a time control of 60s+0.6s and anchored to CCRL 40/4. - * #### Move Overhead - Assume a time delay of x ms due to network and GUI overheads. This is useful to - avoid losses on time in those cases. - - * #### Minimum Thinking Time - Search for at least x ms per move. - - * #### Slow Mover - Lower values will make Stockfish take less time in games, higher values will - make it think longer. - - * #### nodestime - Tells the engine to use nodes searched instead of wall time to account for - elapsed time. Useful for engine testing. - - * #### UCI_Chess960 - An option handled by your GUI. If true, Stockfish will play Chess960. - - * #### UCI_AnalyseMode - An option handled by your GUI. + * #### Skill Level + Lower the Skill Level in order to make Stockfish play weaker (see also UCI_LimitStrength). + Internally, MultiPV is enabled, and with a certain probability depending on the Skill Level a + weaker move will be played. * #### SyzygyPath Path to the folders/directories storing the Syzygy tablebase files. Multiple @@ -101,8 +109,8 @@ Currently, Stockfish has the following UCI options: * #### SyzygyProbeDepth Minimum remaining search depth for which a position is probed. Set this option - to a higher value to probe less agressively if you experience too much slowdown - (in terms of nps) due to TB probing. + to a higher value to probe less aggressively if you experience too much slowdown + (in terms of nps) due to tablebase probing. * #### Syzygy50MoveRule Disable to let fifty-move rule draws detected by Syzygy tablebase probes count @@ -112,13 +120,94 @@ Currently, Stockfish has the following UCI options: Limit Syzygy tablebase probing to positions with at most this many pieces left (including kings and pawns). + * #### Move Overhead + Assume a time delay of x ms due to network and GUI overheads. This is useful to + avoid losses on time in those cases. + + * #### Slow Mover + Lower values will make Stockfish take less time in games, higher values will + make it think longer. + + * #### nodestime + Tells the engine to use nodes searched instead of wall time to account for + elapsed time. Useful for engine testing. + + * #### Debug Log File + Write all communication to and from the engine into a text file. + +For developers the following non-standard commands might be of interest, mainly useful for debugging: + + * #### bench *ttSize threads limit fenFile limitType evalType* + Performs a standard benchmark using various options. The signature of a version (standard node + count) is obtained using all defaults. `bench` is currently `bench 16 1 13 default depth mixed`. + + * #### compiler + Give information about the compiler and environment used for building a binary. + + * #### d + Display the current position, with ascii art and fen. + + * #### eval + Return the evaluation of the current position. + + * #### export_net [filename] + Exports the currently loaded network to a file. + If the currently loaded network is the embedded network and the filename + is not specified then the network is saved to the file matching the name + of the embedded network, as defined in evaluate.h. + If the currently loaded network is not the embedded network (some net set + through the UCI setoption) then the filename parameter is required and the + network is saved into that file. -## What to expect from Syzygybases? + * #### flip + Flips the side to move. + + +## A note on classical evaluation versus NNUE evaluation + +Both approaches assign a value to a position that is used in alpha-beta (PVS) search +to find the best move. The classical evaluation computes this value as a function +of various chess concepts, handcrafted by experts, tested and tuned using fishtest. +The NNUE evaluation computes this value with a neural network based on basic +inputs (e.g. piece positions only). The network is optimized and trained +on the evaluations of millions of positions at moderate search depth. + +The NNUE evaluation was first introduced in shogi, and ported to Stockfish afterward. +It can be evaluated efficiently on CPUs, and exploits the fact that only parts +of the neural network need to be updated after a typical chess move. +[The nodchip repository](https://github.com/nodchip/Stockfish) provided the first version of +the needed tools to train and develop the NNUE networks. Today, more advanced training tools are available +in [the nnue-pytorch repository](https://github.com/glinscott/nnue-pytorch/), while data generation tools +are available in [a dedicated branch](https://github.com/official-stockfish/Stockfish/tree/tools). + +On CPUs supporting modern vector instructions +(avx2 and similar), the NNUE evaluation results in much stronger playing strength, even +if the nodes per second computed by the engine is somewhat lower (roughly 80% of nps +is typical). + +Notes: + +1) the NNUE evaluation depends on the Stockfish binary and the network parameter +file (see the EvalFile UCI option). Not every parameter file is compatible with a given +Stockfish binary, but the default value of the EvalFile UCI option is the name of a network +that is guaranteed to be compatible with that binary. + +2) to use the NNUE evaluation, the additional data file with neural network parameters +needs to be available. Normally, this file is already embedded in the binary or it +can be downloaded. The filename for the default (recommended) net can be found as the default +value of the `EvalFile` UCI option, with the format `nn-[SHA256 first 12 digits].nnue` +(for instance, `nn-c157e0a5755b.nnue`). This file can be downloaded from +``` +https://tests.stockfishchess.org/api/nn/[filename] +``` +replacing `[filename]` as needed. + +## What to expect from the Syzygy tablebases? If the engine is searching a position that is not in the tablebases (e.g. a position with 8 pieces), it will access the tablebases during the search. If the engine reports a very large score (typically 153.xx), this means -that it has found a winning line into a tablebase position. +it has found a winning line into a tablebase position. If the engine is given a position to search that is in the tablebases, it will use the tablebases at the beginning of the search to preselect all @@ -126,46 +215,76 @@ good moves, i.e. all moves that preserve the win or preserve the draw while taking into account the 50-move rule. It will then perform a search only on those moves. **The engine will not move immediately**, unless there is only a single good move. **The engine likely -will not report a mate score even if the position is known to be won.** +will not report a mate score, even if the position is known to be won.** It is therefore clear that this behaviour is not identical to what one might be used to with Nalimov tablebases. There are technical reasons for this difference, the main technical reason being that Nalimov tablebases use the -DTM metric (distance-to-mate), while Syzygybases use a variation of the +DTM metric (distance-to-mate), while the Syzygy tablebases use a variation of the DTZ metric (distance-to-zero, zero meaning any move that resets the 50-move -counter). This special metric is one of the reasons that Syzygybases are +counter). This special metric is one of the reasons that the Syzygy tablebases are more compact than Nalimov tablebases, while still storing all information needed for optimal play and in addition being able to take into account the 50-move rule. +## Large Pages + +Stockfish supports large pages on Linux and Windows. Large pages make +the hash access more efficient, improving the engine speed, especially +on large hash sizes. Typical increases are 5..10% in terms of nodes per +second, but speed increases up to 30% have been measured. The support is +automatic. Stockfish attempts to use large pages when available and +will fall back to regular memory allocation when this is not the case. + +### Support on Linux + +Large page support on Linux is obtained by the Linux kernel +transparent huge pages functionality. Typically, transparent huge pages +are already enabled, and no configuration is needed. + +### Support on Windows + +The use of large pages requires "Lock Pages in Memory" privilege. See +[Enable the Lock Pages in Memory Option (Windows)](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/enable-the-lock-pages-in-memory-option-windows) +on how to enable this privilege, then run [RAMMap](https://docs.microsoft.com/en-us/sysinternals/downloads/rammap) +to double-check that large pages are used. We suggest that you reboot +your computer after you have enabled large pages, because long Windows +sessions suffer from memory fragmentation, which may prevent Stockfish +from getting large pages: a fresh session is better in this regard. ## Compiling Stockfish yourself from the sources -On Unix-like systems, it should be possible to compile Stockfish -directly from the source code with the included Makefile. +Stockfish has support for 32 or 64-bit CPUs, certain hardware +instructions, big-endian machines such as Power PC, and other platforms. + +On Unix-like systems, it should be easy to compile Stockfish +directly from the source code with the included Makefile in the folder +`src`. In general it is recommended to run `make help` to see a list of make +targets with corresponding descriptions. -Stockfish has support for 32 or 64-bit CPUs, the hardware POPCNT -instruction, big-endian machines such as Power PC, and other platforms. +``` + cd src + make help + make net + make build ARCH=x86-64-modern +``` -In general it is recommended to run `make help` to see a list of make -targets with corresponding descriptions. When not using the Makefile to -compile (for instance with Microsoft MSVC) you need to manually -set/unset some switches in the compiler command line; see file *types.h* -for a quick reference. +When not using the Makefile to compile (for instance, with Microsoft MSVC) you +need to manually set/unset some switches in the compiler command line; see +file *types.h* for a quick reference. -When reporting an issue or a bug, please tell us which version and -compiler you used to create your executable. These informations can -be found by typing the following commands in a console: +When reporting an issue or a bug, please tell us which Stockfish version +and which compiler you used to create your executable. This information +can be found by typing the following command in a console: ``` - ./stockfish - compiler + ./stockfish compiler ``` ## Understanding the code base and participating in the project -Stockfish's improvement over the last couple of years has been a great -community effort. There are a few ways to help contribute to its growth. +Stockfish's improvement over the last decade has been a great community +effort. There are a few ways to help contribute to its growth. ### Donating hardware @@ -186,8 +305,9 @@ generic rather than being focused on Stockfish's precise implementation. Nevertheless, a helpful resource. * The latest source can always be found on [GitHub](https://github.com/official-stockfish/Stockfish). -Discussions about Stockfish take place in the [FishCooking](https://groups.google.com/forum/#!forum/fishcooking) -group and engine testing is done on [Fishtest](https://tests.stockfishchess.org/tests). +Discussions about Stockfish take place these days mainly in the [FishCooking](https://groups.google.com/forum/#!forum/fishcooking) +group and on the [Stockfish Discord channel](https://discord.gg/nv8gDtt). +The engine testing is done on [Fishtest](https://tests.stockfishchess.org/tests). If you want to help improve Stockfish, please read this [guideline](https://github.com/glinscott/fishtest/wiki/Creating-my-first-test) first, where the basics of Stockfish development are explained. @@ -195,16 +315,17 @@ first, where the basics of Stockfish development are explained. ## Terms of use Stockfish is free, and distributed under the **GNU General Public License version 3** -(GPL v3). Essentially, this means that you are free to do almost exactly +(GPL v3). Essentially, this means you are free to do almost exactly what you want with the program, including distributing it among your -friends, making it available for download from your web site, selling +friends, making it available for download from your website, selling it (either by itself or as part of some bigger software package), or using it as the starting point for a software project of your own. The only real limitation is that whenever you distribute Stockfish in -some way, you must always include the full source code, or a pointer -to where the source code can be found. If you make any changes to the -source code, these changes must also be made available under the GPL. +some way, you MUST always include the full source code, or a pointer +to where the source code can be found, to generate the exact binary +you are distributing. If you make any changes to the source code, +these changes must also be made available under the GPL. For full details, read the copy of the GPL v3 found in the file named -*Copying.txt*. +[*Copying.txt*](https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt). diff --git a/src/Makefile b/src/Makefile index 679eb8d90fc..5f3e739f788 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,7 +1,5 @@ # Stockfish, a UCI chess playing engine derived from Glaurung 2.1 -# Copyright (C) 2004-2008 Tord Romstad (Glaurung author) -# Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad -# Copyright (C) 2015-2019 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad +# Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) # # Stockfish is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,11 +19,27 @@ ### Section 1. General Configuration ### ========================================================================== +### Establish the operating system name +KERNEL = $(shell uname -s) +ifeq ($(KERNEL),Linux) + OS = $(shell uname -o) +endif + +### Target Windows OS +ifeq ($(OS),Windows_NT) + target_windows = yes +else ifeq ($(COMP),mingw) + target_windows = yes + ifeq ($(WINE_PATH),) + WINE_PATH = $(shell which wine) + endif +endif + ### Executable name -ifeq ($(COMP),mingw) -EXE = stockfish.exe +ifeq ($(target_windows),yes) + EXE = stockfish.exe else -EXE = stockfish + EXE = stockfish endif ### Installation dir definitions @@ -33,122 +47,304 @@ PREFIX = /usr/local BINDIR = $(PREFIX)/bin ### Built-in benchmark for pgo-builds -PGOBENCH = ./$(EXE) bench +ifeq ($(SDE_PATH),) + PGOBENCH = $(WINE_PATH) ./$(EXE) bench +else + PGOBENCH = $(SDE_PATH) -- $(WINE_PATH) ./$(EXE) bench +endif -### Object files -OBJS = benchmark.o bitbase.o bitboard.o endgame.o evaluate.o main.o \ - material.o misc.o movegen.o movepick.o pawns.o position.o psqt.o \ - search.o thread.o timeman.o tt.o uci.o ucioption.o syzygy/tbprobe.o +### Source and object files +SRCS = benchmark.cpp bitbase.cpp bitboard.cpp endgame.cpp evaluate.cpp main.cpp \ + material.cpp misc.cpp movegen.cpp movepick.cpp pawns.cpp position.cpp psqt.cpp \ + search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \ + nnue/evaluate_nnue.cpp nnue/features/half_ka_v2_hm.cpp -### Establish the operating system name -KERNEL = $(shell uname -s) -ifeq ($(KERNEL),Linux) - OS = $(shell uname -o) -endif +OBJS = $(notdir $(SRCS:.cpp=.o)) + +VPATH = syzygy:nnue:nnue/features ### ========================================================================== ### Section 2. High-level Configuration ### ========================================================================== # -# flag --- Comp switch --- Description +# flag --- Comp switch --- Description # ---------------------------------------------------------------------------- # # debug = yes/no --- -DNDEBUG --- Enable/Disable debug mode -# sanitize = undefined/thread/no (-fsanitize ) +# sanitize = none/ ... (-fsanitize ) # --- ( undefined ) --- enable undefined behavior checks -# --- ( thread ) --- enable threading error checks +# --- ( thread ) --- enable threading error checks +# --- ( address ) --- enable memory access checks +# --- ...etc... --- see compiler documentation for supported sanitizers # optimize = yes/no --- (-O3/-fast etc.) --- Enable/Disable optimizations # arch = (name) --- (-arch) --- Target architecture # bits = 64/32 --- -DIS_64BIT --- 64-/32-bit operating system # prefetch = yes/no --- -DUSE_PREFETCH --- Use prefetch asm-instruction # popcnt = yes/no --- -DUSE_POPCNT --- Use popcnt asm-instruction -# sse = yes/no --- -msse --- Use Intel Streaming SIMD Extensions # pext = yes/no --- -DUSE_PEXT --- Use pext x86_64 asm-instruction +# sse = yes/no --- -msse --- Use Intel Streaming SIMD Extensions +# mmx = yes/no --- -mmmx --- Use Intel MMX instructions +# sse2 = yes/no --- -msse2 --- Use Intel Streaming SIMD Extensions 2 +# ssse3 = yes/no --- -mssse3 --- Use Intel Supplemental Streaming SIMD Extensions 3 +# sse41 = yes/no --- -msse4.1 --- Use Intel Streaming SIMD Extensions 4.1 +# avx2 = yes/no --- -mavx2 --- Use Intel Advanced Vector Extensions 2 +# avxvnni = yes/no --- -mavxvnni --- Use Intel Vector Neural Network Instructions AVX +# avx512 = yes/no --- -mavx512bw --- Use Intel Advanced Vector Extensions 512 +# vnni256 = yes/no --- -mavx512vnni --- Use Intel Vector Neural Network Instructions 256 +# vnni512 = yes/no --- -mavx512vnni --- Use Intel Vector Neural Network Instructions 512 +# neon = yes/no --- -DUSE_NEON --- Use ARM SIMD architecture # # Note that Makefile is space sensitive, so when adding new architectures # or modifying existing flags, you have to make sure there are no extra spaces # at the end of the line for flag values. +# +# Example of use for these flags: +# make build ARCH=x86-64-avx512 debug=yes sanitize="address undefined" + ### 2.1. General and architecture defaults + +ifeq ($(ARCH),) + ARCH = x86-64-modern + help_skip_sanity = yes +endif +# explicitly check for the list of supported architectures (as listed with make help), +# the user can override with `make ARCH=x86-32-vnni256 SUPPORTED_ARCH=true` +ifeq ($(ARCH), $(filter $(ARCH), \ + x86-64-vnni512 x86-64-vnni256 x86-64-avx512 x86-64-avxvnni x86-64-bmi2 \ + x86-64-avx2 x86-64-sse41-popcnt x86-64-modern x86-64-ssse3 x86-64-sse3-popcnt \ + x86-64 x86-32-sse41-popcnt x86-32-sse2 x86-32 ppc-64 ppc-32 e2k \ + armv7 armv7-neon armv8 apple-silicon general-64 general-32)) + SUPPORTED_ARCH=true +else + SUPPORTED_ARCH=false +endif + optimize = yes debug = no -sanitize = no -bits = 32 +sanitize = none +bits = 64 prefetch = no popcnt = no -sse = no pext = no +sse = no +mmx = no +sse2 = no +ssse3 = no +sse41 = no +avx2 = no +avxvnni = no +avx512 = no +vnni256 = no +vnni512 = no +neon = no +arm_version = 0 +STRIP = strip ### 2.2 Architecture specific -ifeq ($(ARCH),general-32) - arch = any -endif +ifeq ($(findstring x86,$(ARCH)),x86) + +# x86-32/64 -ifeq ($(ARCH),x86-32-old) +ifeq ($(findstring x86-32,$(ARCH)),x86-32) arch = i386 + bits = 32 + sse = yes + mmx = yes +else + arch = x86_64 + sse = yes + sse2 = yes endif -ifeq ($(ARCH),x86-32) - arch = i386 - prefetch = yes +ifeq ($(findstring -sse,$(ARCH)),-sse) sse = yes endif -ifeq ($(ARCH),general-64) - arch = any - bits = 64 +ifeq ($(findstring -popcnt,$(ARCH)),-popcnt) + popcnt = yes endif -ifeq ($(ARCH),x86-64) - arch = x86_64 - bits = 64 - prefetch = yes +ifeq ($(findstring -mmx,$(ARCH)),-mmx) + mmx = yes +endif + +ifeq ($(findstring -sse2,$(ARCH)),-sse2) sse = yes + sse2 = yes endif -ifeq ($(ARCH),x86-64-modern) - arch = x86_64 - bits = 64 - prefetch = yes +ifeq ($(findstring -ssse3,$(ARCH)),-ssse3) + sse = yes + sse2 = yes + ssse3 = yes +endif + +ifeq ($(findstring -sse41,$(ARCH)),-sse41) + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes +endif + +ifeq ($(findstring -modern,$(ARCH)),-modern) popcnt = yes sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes endif -ifeq ($(ARCH),x86-64-bmi2) - arch = x86_64 - bits = 64 - prefetch = yes +ifeq ($(findstring -avx2,$(ARCH)),-avx2) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes +endif + +ifeq ($(findstring -avxvnni,$(ARCH)),-avxvnni) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + avxvnni = yes + pext = yes +endif + +ifeq ($(findstring -bmi2,$(ARCH)),-bmi2) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes +endif + +ifeq ($(findstring -avx512,$(ARCH)),-avx512) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + avx512 = yes +endif + +ifeq ($(findstring -vnni256,$(ARCH)),-vnni256) popcnt = yes sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes pext = yes + vnni256 = yes +endif + +ifeq ($(findstring -vnni512,$(ARCH)),-vnni512) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + avx512 = yes + vnni512 = yes +endif + +ifeq ($(sse),yes) + prefetch = yes +endif + +# 64-bit pext is not available on x86-32 +ifeq ($(bits),32) + pext = no +endif + +else + +# all other architectures + +ifeq ($(ARCH),general-32) + arch = any + bits = 32 +endif + +ifeq ($(ARCH),general-64) + arch = any endif ifeq ($(ARCH),armv7) arch = armv7 prefetch = yes + bits = 32 + arm_version = 7 +endif + +ifeq ($(ARCH),armv7-neon) + arch = armv7 + prefetch = yes + popcnt = yes + neon = yes + bits = 32 + arm_version = 7 +endif + +ifeq ($(ARCH),armv8) + arch = armv8 + prefetch = yes + popcnt = yes + neon = yes + arm_version = 8 +endif + +ifeq ($(ARCH),apple-silicon) + arch = arm64 + prefetch = yes + popcnt = yes + neon = yes + arm_version = 8 endif ifeq ($(ARCH),ppc-32) arch = ppc + bits = 32 endif ifeq ($(ARCH),ppc-64) arch = ppc64 - bits = 64 popcnt = yes prefetch = yes endif +ifeq ($(findstring e2k,$(ARCH)),e2k) + arch = e2k + mmx = yes + bits = 64 + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + popcnt = yes +endif + +endif ### ========================================================================== -### Section 3. Low-level configuration +### Section 3. Low-level Configuration ### ========================================================================== ### 3.1 Selecting compiler (default = gcc) - -CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++11 $(EXTRACXXFLAGS) -DEPENDFLAGS += -std=c++11 +CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++17 $(EXTRACXXFLAGS) +DEPENDFLAGS += -std=c++17 LDFLAGS += $(EXTRALDFLAGS) ifeq ($(COMP),) @@ -160,7 +356,7 @@ ifeq ($(COMP),gcc) CXX=g++ CXXFLAGS += -pedantic -Wextra -Wshadow - ifeq ($(ARCH),armv7) + ifeq ($(arch),$(filter $(arch),armv7 armv8)) ifeq ($(OS),Android) CXXFLAGS += -m$(bits) LDFLAGS += -m$(bits) @@ -170,34 +366,36 @@ ifeq ($(COMP),gcc) LDFLAGS += -m$(bits) endif + ifeq ($(arch),$(filter $(arch),armv7)) + LDFLAGS += -latomic + endif + ifneq ($(KERNEL),Darwin) LDFLAGS += -Wl,--no-as-needed endif endif +ifeq ($(target_windows),yes) + LDFLAGS += -static +endif + ifeq ($(COMP),mingw) comp=mingw - ifeq ($(KERNEL),Linux) - ifeq ($(bits),64) - ifeq ($(shell which x86_64-w64-mingw32-c++-posix),) - CXX=x86_64-w64-mingw32-c++ - else - CXX=x86_64-w64-mingw32-c++-posix - endif + ifeq ($(bits),64) + ifeq ($(shell which x86_64-w64-mingw32-c++-posix 2> /dev/null),) + CXX=x86_64-w64-mingw32-c++ else - ifeq ($(shell which i686-w64-mingw32-c++-posix),) - CXX=i686-w64-mingw32-c++ - else - CXX=i686-w64-mingw32-c++-posix - endif + CXX=x86_64-w64-mingw32-c++-posix endif else - CXX=g++ + ifeq ($(shell which i686-w64-mingw32-c++-posix 2> /dev/null),) + CXX=i686-w64-mingw32-c++ + else + CXX=i686-w64-mingw32-c++-posix + endif endif - - CXXFLAGS += -Wextra -Wshadow - LDFLAGS += -static + CXXFLAGS += -pedantic -Wextra -Wshadow endif ifeq ($(COMP),icc) @@ -209,15 +407,21 @@ endif ifeq ($(COMP),clang) comp=clang CXX=clang++ + ifeq ($(target_windows),yes) + CXX=x86_64-w64-mingw32-clang++ + endif + CXXFLAGS += -pedantic -Wextra -Wshadow - ifneq ($(KERNEL),Darwin) - ifneq ($(KERNEL),OpenBSD) + ifeq ($(filter $(KERNEL),Darwin OpenBSD FreeBSD),) + ifeq ($(target_windows),) + ifneq ($(RTLIB),compiler-rt) LDFLAGS += -latomic endif endif + endif - ifeq ($(ARCH),armv7) + ifeq ($(arch),$(filter $(arch),armv7 armv8)) ifeq ($(OS),Android) CXXFLAGS += -m$(bits) LDFLAGS += -m$(bits) @@ -228,22 +432,46 @@ ifeq ($(COMP),clang) endif endif +ifeq ($(KERNEL),Darwin) + CXXFLAGS += -mmacosx-version-min=10.14 + LDFLAGS += -mmacosx-version-min=10.14 + ifneq ($(arch),any) + CXXFLAGS += -arch $(arch) + LDFLAGS += -arch $(arch) + endif + XCRUN = xcrun +endif + +# To cross-compile for Android, NDK version r21 or later is recommended. +# In earlier NDK versions, you'll need to pass -fno-addrsig if using GNU binutils. +# Currently we don't know how to make PGO builds with the NDK yet. +ifeq ($(COMP),ndk) + CXXFLAGS += -stdlib=libc++ -fPIE + comp=clang + ifeq ($(arch),armv7) + CXX=armv7a-linux-androideabi16-clang++ + CXXFLAGS += -mthumb -march=armv7-a -mfloat-abi=softfp -mfpu=neon + STRIP=arm-linux-androideabi-strip + endif + ifeq ($(arch),armv8) + CXX=aarch64-linux-android21-clang++ + STRIP=aarch64-linux-android-strip + endif + LDFLAGS += -static-libstdc++ -pie -lm -latomic +endif + ifeq ($(comp),icc) profile_make = icc-profile-make profile_use = icc-profile-use -else -ifeq ($(comp),clang) +else ifeq ($(comp),clang) profile_make = clang-profile-make profile_use = clang-profile-use else profile_make = gcc-profile-make profile_use = gcc-profile-use -endif -endif - -ifeq ($(KERNEL),Darwin) - CXXFLAGS += -arch $(arch) -mmacosx-version-min=10.9 - LDFLAGS += -arch $(arch) -mmacosx-version-min=10.9 + ifeq ($(KERNEL),Darwin) + EXTRAPROFILEFLAGS = -fvisibility=hidden + endif endif ### Travis CI script uses COMPILER to overwrite CXX @@ -256,13 +484,26 @@ ifdef COMPCXX CXX=$(COMPCXX) endif +### Sometimes gcc is really clang +ifeq ($(COMP),gcc) + gccversion = $(shell $(CXX) --version) + gccisclang = $(findstring clang,$(gccversion)) + ifneq ($(gccisclang),) + profile_make = clang-profile-make + profile_use = clang-profile-use + endif +endif + ### On mingw use Windows threads, otherwise POSIX ifneq ($(comp),mingw) + CXXFLAGS += -DUSE_PTHREADS # On Android Bionic's C library comes with its own pthread implementation bundled in ifneq ($(OS),Android) # Haiku has pthreads in its libroot, so only link it in on other platforms ifneq ($(KERNEL),Haiku) - LDFLAGS += -lpthread + ifneq ($(COMP),ndk) + LDFLAGS += -lpthread + endif endif endif endif @@ -275,9 +516,9 @@ else endif ### 3.2.2 Debugging with undefined behavior sanitizers -ifneq ($(sanitize),no) - CXXFLAGS += -g3 -fsanitize=$(sanitize) -fuse-ld=gold - LDFLAGS += -fsanitize=$(sanitize) -fuse-ld=gold +ifneq ($(sanitize),none) + CXXFLAGS += -g3 $(addprefix -fsanitize=,$(sanitize)) + LDFLAGS += $(addprefix -fsanitize=,$(sanitize)) endif ### 3.3 Optimization @@ -291,10 +532,20 @@ ifeq ($(optimize),yes) endif endif - ifeq ($(comp),$(filter $(comp),gcc clang icc)) - ifeq ($(KERNEL),Darwin) - CXXFLAGS += -mdynamic-no-pic - endif + ifeq ($(KERNEL),Darwin) + ifeq ($(comp),$(filter $(comp),clang icc)) + CXXFLAGS += -mdynamic-no-pic + endif + + ifeq ($(comp),gcc) + ifneq ($(arch),arm64) + CXXFLAGS += -mdynamic-no-pic + endif + endif + endif + + ifeq ($(comp),clang) + CXXFLAGS += -fexperimental-new-pass-manager endif endif @@ -303,19 +554,17 @@ ifeq ($(bits),64) CXXFLAGS += -DIS_64BIT endif -### 3.5 prefetch +### 3.5 prefetch and popcount ifeq ($(prefetch),yes) ifeq ($(sse),yes) CXXFLAGS += -msse - DEPENDFLAGS += -msse endif else CXXFLAGS += -DNO_PREFETCH endif -### 3.6 popcnt ifeq ($(popcnt),yes) - ifeq ($(arch),ppc64) + ifeq ($(arch),$(filter $(arch),ppc64 armv7 armv8 arm64)) CXXFLAGS += -DUSE_POPCNT else ifeq ($(comp),icc) CXXFLAGS += -msse3 -DUSE_POPCNT @@ -324,29 +573,119 @@ ifeq ($(popcnt),yes) endif endif +### 3.6 SIMD architectures +ifeq ($(avx2),yes) + CXXFLAGS += -DUSE_AVX2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mavx2 + endif +endif + +ifeq ($(avxvnni),yes) + CXXFLAGS += -DUSE_VNNI -DUSE_AVXVNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mavxvnni + endif +endif + +ifeq ($(avx512),yes) + CXXFLAGS += -DUSE_AVX512 + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mavx512f -mavx512bw + endif +endif + +ifeq ($(vnni256),yes) + CXXFLAGS += -DUSE_VNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl -mprefer-vector-width=256 + endif +endif + +ifeq ($(vnni512),yes) + CXXFLAGS += -DUSE_VNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mavx512vnni -mavx512dq -mavx512vl + endif +endif + +ifeq ($(sse41),yes) + CXXFLAGS += -DUSE_SSE41 + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -msse4.1 + endif +endif + +ifeq ($(ssse3),yes) + CXXFLAGS += -DUSE_SSSE3 + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mssse3 + endif +endif + +ifeq ($(sse2),yes) + CXXFLAGS += -DUSE_SSE2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -msse2 + endif +endif + +ifeq ($(mmx),yes) + CXXFLAGS += -DUSE_MMX + ifeq ($(comp),$(filter $(comp),gcc clang mingw)) + CXXFLAGS += -mmmx + endif +endif + +ifeq ($(neon),yes) + CXXFLAGS += -DUSE_NEON=$(arm_version) + ifeq ($(KERNEL),Linux) + ifneq ($(COMP),ndk) + ifneq ($(arch),armv8) + CXXFLAGS += -mfpu=neon + endif + endif + endif +endif + ### 3.7 pext ifeq ($(pext),yes) CXXFLAGS += -DUSE_PEXT ifeq ($(comp),$(filter $(comp),gcc clang mingw)) - CXXFLAGS += -msse4 -mbmi2 + CXXFLAGS += -mbmi2 endif endif -### 3.8 Link Time Optimization, it works since gcc 4.5 but not on mingw under Windows. +### 3.8 Link Time Optimization ### This is a mix of compile and link time options because the lto link phase ### needs access to the optimization flags. ifeq ($(optimize),yes) ifeq ($(debug), no) - ifeq ($(comp),$(filter $(comp),gcc clang)) + ifeq ($(comp),clang) CXXFLAGS += -flto + ifeq ($(target_windows),yes) + CXXFLAGS += -fuse-ld=lld + endif LDFLAGS += $(CXXFLAGS) - endif - ifeq ($(comp),mingw) - ifeq ($(KERNEL),Linux) +# GCC and CLANG use different methods for parallelizing LTO and CLANG pretends to be +# GCC on some systems. + else ifeq ($(comp),gcc) + ifeq ($(gccisclang),) + CXXFLAGS += -flto + LDFLAGS += $(CXXFLAGS) -flto=jobserver + else CXXFLAGS += -flto LDFLAGS += $(CXXFLAGS) endif + +# To use LTO and static linking on Windows, +# the tool chain requires gcc version 10.1 or later. + else ifeq ($(comp),mingw) + ifneq ($(arch),i386) + CXXFLAGS += -flto + LDFLAGS += $(CXXFLAGS) -save-temps + endif endif endif endif @@ -358,11 +697,11 @@ ifeq ($(OS), Android) LDFLAGS += -fPIE -pie endif - ### ========================================================================== -### Section 4. Public targets +### Section 4. Public Targets ### ========================================================================== + help: @echo "" @echo "To compile stockfish, type: " @@ -371,22 +710,37 @@ help: @echo "" @echo "Supported targets:" @echo "" + @echo "help > Display architecture details" @echo "build > Standard build" - @echo "profile-build > PGO build" + @echo "net > Download the default nnue net" + @echo "profile-build > Faster build (with profile-guided optimization)" @echo "strip > Strip executable" @echo "install > Install executable" @echo "clean > Clean up" @echo "" @echo "Supported archs:" @echo "" - @echo "x86-64-bmi2 > x86 64-bit with pext support (also enables SSE4)" - @echo "x86-64-modern > x86 64-bit with popcnt support (also enables SSE3)" - @echo "x86-64 > x86 64-bit generic" - @echo "x86-32 > x86 32-bit (also enables SSE)" - @echo "x86-32-old > x86 32-bit fall back for old hardware" + @echo "x86-64-vnni512 > x86 64-bit with vnni support 512bit wide" + @echo "x86-64-vnni256 > x86 64-bit with vnni support 256bit wide" + @echo "x86-64-avx512 > x86 64-bit with avx512 support" + @echo "x86-64-avxvnni > x86 64-bit with avxvnni support" + @echo "x86-64-bmi2 > x86 64-bit with bmi2 support" + @echo "x86-64-avx2 > x86 64-bit with avx2 support" + @echo "x86-64-sse41-popcnt > x86 64-bit with sse41 and popcnt support" + @echo "x86-64-modern > common modern CPU, currently x86-64-sse41-popcnt" + @echo "x86-64-ssse3 > x86 64-bit with ssse3 support" + @echo "x86-64-sse3-popcnt > x86 64-bit with sse3 and popcnt support" + @echo "x86-64 > x86 64-bit generic (with sse2 support)" + @echo "x86-32-sse41-popcnt > x86 32-bit with sse41 and popcnt support" + @echo "x86-32-sse2 > x86 32-bit with sse2 support" + @echo "x86-32 > x86 32-bit generic (with mmx and sse support)" @echo "ppc-64 > PPC 64-bit" @echo "ppc-32 > PPC 32-bit" @echo "armv7 > ARMv7 32-bit" + @echo "armv7-neon > ARMv7 32-bit with popcnt and neon" + @echo "armv8 > ARMv8 64-bit with popcnt and neon" + @echo "e2k > Elbrus 2000" + @echo "apple-silicon > Apple silicon ARM64" @echo "general-64 > unspecified 64-bit" @echo "general-32 > unspecified 32-bit" @echo "" @@ -396,33 +750,43 @@ help: @echo "mingw > Gnu compiler with MinGW under Windows" @echo "clang > LLVM Clang compiler" @echo "icc > Intel compiler" + @echo "ndk > Google NDK to cross-compile for Android" @echo "" @echo "Simple examples. If you don't know what to do, you likely want to run: " @echo "" - @echo "make build ARCH=x86-64 (This is for 64-bit systems)" - @echo "make build ARCH=x86-32 (This is for 32-bit systems)" + @echo "make -j build ARCH=x86-64 (A portable, slow compile for 64-bit systems)" + @echo "make -j build ARCH=x86-32 (A portable, slow compile for 32-bit systems)" @echo "" - @echo "Advanced examples, for experienced users: " + @echo "Advanced examples, for experienced users looking for performance: " @echo "" - @echo "make build ARCH=x86-64 COMP=clang" - @echo "make profile-build ARCH=x86-64-bmi2 COMP=gcc COMPCXX=g++-4.8" + @echo "make help ARCH=x86-64-bmi2" + @echo "make -j profile-build ARCH=x86-64-bmi2 COMP=gcc COMPCXX=g++-9.0" + @echo "make -j build ARCH=x86-64-ssse3 COMP=clang" @echo "" + @echo "-------------------------------" +ifeq ($(SUPPORTED_ARCH)$(help_skip_sanity), true) + @echo "The selected architecture $(ARCH) will enable the following configuration: " + @$(MAKE) ARCH=$(ARCH) COMP=$(COMP) config-sanity +else + @echo "Specify a supported architecture with the ARCH option for more details" + @echo "" +endif -.PHONY: help build profile-build strip install clean objclean profileclean help \ +.PHONY: help build profile-build strip install clean net objclean profileclean \ config-sanity icc-profile-use icc-profile-make gcc-profile-use gcc-profile-make \ clang-profile-use clang-profile-make -build: config-sanity +build: net config-sanity $(MAKE) ARCH=$(ARCH) COMP=$(COMP) all -profile-build: config-sanity objclean profileclean +profile-build: net config-sanity objclean profileclean @echo "" @echo "Step 1/4. Building instrumented executable ..." $(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_make) @echo "" @echo "Step 2/4. Running benchmark for pgo-build ..." - $(PGOBENCH) > /dev/null + $(PGOBENCH) 2>&1 | tail -n 4 @echo "" @echo "Step 3/4. Building optimized executable ..." $(MAKE) ARCH=$(ARCH) COMP=$(COMP) objclean @@ -432,37 +796,65 @@ profile-build: config-sanity objclean profileclean $(MAKE) ARCH=$(ARCH) COMP=$(COMP) profileclean strip: - strip $(EXE) + $(STRIP) $(EXE) install: -mkdir -p -m 755 $(BINDIR) -cp $(EXE) $(BINDIR) -strip $(BINDIR)/$(EXE) -#clean all +# clean all clean: objclean profileclean @rm -f .depend *~ core +# evaluation network (nnue) +net: + $(eval nnuenet := $(shell grep EvalFileDefaultName evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) + @echo "Default net: $(nnuenet)" + $(eval nnuedownloadurl := https://tests.stockfishchess.org/api/nn/$(nnuenet)) + $(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi)) + @if test -f "$(nnuenet)"; then \ + echo "Already available."; \ + else \ + if [ "x$(curl_or_wget)" = "x" ]; then \ + echo "Automatic download failed: neither curl nor wget is installed. Install one of these tools or download the net manually"; exit 1; \ + else \ + echo "Downloading $(nnuedownloadurl)"; $(curl_or_wget) $(nnuedownloadurl) > $(nnuenet);\ + fi; \ + fi; + $(eval shasum_command := $(shell if hash shasum 2>/dev/null; then echo "shasum -a 256 "; elif hash sha256sum 2>/dev/null; then echo "sha256sum "; fi)) + @if [ "x$(shasum_command)" != "x" ]; then \ + if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ + echo "Failed download or $(nnuenet) corrupted, please delete!"; exit 1; \ + fi \ + else \ + echo "shasum / sha256sum not found, skipping net validation"; \ + fi + # clean binaries and objects objclean: - @rm -f $(EXE) *.o ./syzygy/*.o + @rm -f $(EXE) *.o ./syzygy/*.o ./nnue/*.o ./nnue/features/*.o # clean auxiliary profiling files profileclean: @rm -rf profdir - @rm -f bench.txt *.gcda ./syzygy/*.gcda *.gcno ./syzygy/*.gcno + @rm -f bench.txt *.gcda *.gcno ./syzygy/*.gcda ./nnue/*.gcda ./nnue/features/*.gcda *.s @rm -f stockfish.profdata *.profraw + @rm -f stockfish.*args* + @rm -f stockfish.*lt* + @rm -f stockfish.res + @rm -f ./-lstdc++.res default: help ### ========================================================================== -### Section 5. Private targets +### Section 5. Private Targets ### ========================================================================== all: $(EXE) .depend -config-sanity: +config-sanity: net @echo "" @echo "Config:" @echo "debug: '$(debug)'" @@ -474,8 +866,19 @@ config-sanity: @echo "os: '$(OS)'" @echo "prefetch: '$(prefetch)'" @echo "popcnt: '$(popcnt)'" - @echo "sse: '$(sse)'" @echo "pext: '$(pext)'" + @echo "sse: '$(sse)'" + @echo "mmx: '$(mmx)'" + @echo "sse2: '$(sse2)'" + @echo "ssse3: '$(ssse3)'" + @echo "sse41: '$(sse41)'" + @echo "avx2: '$(avx2)'" + @echo "avxvnni: '$(avxvnni)'" + @echo "avx512: '$(avx512)'" + @echo "vnni256: '$(vnni256)'" + @echo "vnni512: '$(vnni512)'" + @echo "neon: '$(neon)'" + @echo "arm_version: '$(arm_version)'" @echo "" @echo "Flags:" @echo "CXX: $(CXX)" @@ -485,19 +888,30 @@ config-sanity: @echo "Testing config sanity. If this fails, try 'make help' ..." @echo "" @test "$(debug)" = "yes" || test "$(debug)" = "no" - @test "$(sanitize)" = "undefined" || test "$(sanitize)" = "thread" || test "$(sanitize)" = "address" || test "$(sanitize)" = "no" @test "$(optimize)" = "yes" || test "$(optimize)" = "no" + @test "$(SUPPORTED_ARCH)" = "true" @test "$(arch)" = "any" || test "$(arch)" = "x86_64" || test "$(arch)" = "i386" || \ - test "$(arch)" = "ppc64" || test "$(arch)" = "ppc" || test "$(arch)" = "armv7" + test "$(arch)" = "ppc64" || test "$(arch)" = "ppc" || test "$(arch)" = "e2k" || \ + test "$(arch)" = "armv7" || test "$(arch)" = "armv8" || test "$(arch)" = "arm64" @test "$(bits)" = "32" || test "$(bits)" = "64" @test "$(prefetch)" = "yes" || test "$(prefetch)" = "no" @test "$(popcnt)" = "yes" || test "$(popcnt)" = "no" - @test "$(sse)" = "yes" || test "$(sse)" = "no" @test "$(pext)" = "yes" || test "$(pext)" = "no" - @test "$(comp)" = "gcc" || test "$(comp)" = "icc" || test "$(comp)" = "mingw" || test "$(comp)" = "clang" + @test "$(sse)" = "yes" || test "$(sse)" = "no" + @test "$(mmx)" = "yes" || test "$(mmx)" = "no" + @test "$(sse2)" = "yes" || test "$(sse2)" = "no" + @test "$(ssse3)" = "yes" || test "$(ssse3)" = "no" + @test "$(sse41)" = "yes" || test "$(sse41)" = "no" + @test "$(avx2)" = "yes" || test "$(avx2)" = "no" + @test "$(avx512)" = "yes" || test "$(avx512)" = "no" + @test "$(vnni256)" = "yes" || test "$(vnni256)" = "no" + @test "$(vnni512)" = "yes" || test "$(vnni512)" = "no" + @test "$(neon)" = "yes" || test "$(neon)" = "no" + @test "$(comp)" = "gcc" || test "$(comp)" = "icc" || test "$(comp)" = "mingw" || test "$(comp)" = "clang" \ + || test "$(comp)" = "armv7a-linux-androideabi16-clang" || test "$(comp)" = "aarch64-linux-android21-clang" $(EXE): $(OBJS) - $(CXX) -o $@ $(OBJS) $(LDFLAGS) + +$(CXX) -o $@ $(OBJS) $(LDFLAGS) clang-profile-make: $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ @@ -506,21 +920,24 @@ clang-profile-make: all clang-profile-use: - llvm-profdata merge -output=stockfish.profdata *.profraw + $(XCRUN) llvm-profdata merge -output=stockfish.profdata *.profraw $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ EXTRACXXFLAGS='-fprofile-instr-use=stockfish.profdata' \ EXTRALDFLAGS='-fprofile-use ' \ all gcc-profile-make: + @mkdir -p profdir $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ - EXTRACXXFLAGS='-fprofile-generate' \ + EXTRACXXFLAGS='-fprofile-generate=profdir' \ + EXTRACXXFLAGS+=$(EXTRAPROFILEFLAGS) \ EXTRALDFLAGS='-lgcov' \ all gcc-profile-use: $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ - EXTRACXXFLAGS='-fprofile-use -fno-peel-loops -fno-tracer' \ + EXTRACXXFLAGS='-fprofile-use=profdir -fno-peel-loops -fno-tracer' \ + EXTRACXXFLAGS+=$(EXTRAPROFILEFLAGS) \ EXTRALDFLAGS='-lgcov' \ all @@ -535,8 +952,7 @@ icc-profile-use: EXTRACXXFLAGS='-prof_use -prof_dir ./profdir' \ all -.depend: - -@$(CXX) $(DEPENDFLAGS) -MM $(OBJS:.o=.cpp) > $@ 2> /dev/null +.depend: $(SRCS) + -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null -include .depend - diff --git a/src/benchmark.cpp b/src/benchmark.cpp index f906e731b40..e1c025adb9a 100644 --- a/src/benchmark.cpp +++ b/src/benchmark.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -65,6 +63,7 @@ const vector Defaults = { "4rrk1/1p1nq3/p7/2p1P1pp/3P2bp/3Q1Bn1/PPPB4/1K2R1NR w - - 40 21", "r3k2r/3nnpbp/q2pp1p1/p7/Pp1PPPP1/4BNN1/1P5P/R2Q1RK1 w kq - 0 16", "3Qb1k1/1r2ppb1/pN1n2q1/Pp1Pp1Pr/4P2p/4BP2/4B1R1/1R5K b - - 11 40", + "4k3/3q1r2/1N2r1b1/3ppN2/2nPP3/1B1R2n1/2R1Q3/3K4 w - - 5 1", // 5-man positions "8/8/8/8/5kp1/P7/8/1K1N4 w - - 0 1", // Kc2 - mate @@ -87,17 +86,21 @@ const vector Defaults = { // Chess 960 "setoption name UCI_Chess960 value true", - "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR w KQkq - 0 1 moves g2g3 d7d5 d2d4 c8h3 c1g5 e8d6 g5e7 f7f6", + "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR w HFhf - 0 1 moves g2g3 d7d5 d2d4 c8h3 c1g5 e8d6 g5e7 f7f6", + "nqbnrkrb/pppppppp/8/8/8/8/PPPPPPPP/NQBNRKRB w KQkq - 0 1", "setoption name UCI_Chess960 value false" }; } // namespace +namespace Stockfish { + /// setup_bench() builds a list of UCI commands to be run by bench. There /// are five parameters: TT size in MB, number of search threads that /// should be used, the limit value spent for each position, a file name -/// where to look for positions in FEN format and the type of the limit: -/// depth, perft, nodes and movetime (in millisecs). +/// where to look for positions in FEN format, the type of the limit: +/// depth, perft, nodes and movetime (in millisecs), and evaluation type +/// mixed (default), classical, NNUE. /// /// bench -> search default positions up to depth 13 /// bench 64 1 15 -> search default positions up to depth 15 (TT = 64MB) @@ -116,6 +119,7 @@ vector setup_bench(const Position& current, istream& is) { string limit = (is >> token) ? token : "13"; string fenFile = (is >> token) ? token : "default"; string limitType = (is >> token) ? token : "depth"; + string evalType = (is >> token) ? token : "mixed"; go = limitType == "eval" ? "eval" : "go " + limitType + " " + limit; @@ -147,14 +151,25 @@ vector setup_bench(const Position& current, istream& is) { list.emplace_back("setoption name Hash value " + ttSize); list.emplace_back("ucinewgame"); + size_t posCounter = 0; + for (const string& fen : fens) if (fen.find("setoption") != string::npos) list.emplace_back(fen); else { + if (evalType == "classical" || (evalType == "mixed" && posCounter % 2 == 0)) + list.emplace_back("setoption name Use NNUE value false"); + else if (evalType == "NNUE" || (evalType == "mixed" && posCounter % 2 != 0)) + list.emplace_back("setoption name Use NNUE value true"); list.emplace_back("position fen " + fen); list.emplace_back(go); + ++posCounter; } + list.emplace_back("setoption name Use NNUE value true"); + return list; } + +} // namespace Stockfish diff --git a/src/bitbase.cpp b/src/bitbase.cpp index 78614fa2acb..84300baf92a 100644 --- a/src/bitbase.cpp +++ b/src/bitbase.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,20 +17,21 @@ */ #include -#include #include +#include #include "bitboard.h" #include "types.h" +namespace Stockfish { + namespace { // There are 24 possible pawn squares: files A to D and ranks from 2 to 7. // Positions with the pawn on files E to H will be mirrored before probing. constexpr unsigned MAX_INDEX = 2*24*64*64; // stm * psq * wksq * bksq = 196608 - // Each uint32_t stores results of 32 positions, one per bit - uint32_t KPKBitbase[MAX_INDEX / 32]; + std::bitset KPKBitbase; // A KPK bitbase index is an integer in [0, IndexMax] range // @@ -43,8 +42,8 @@ namespace { // bit 12: side to move (WHITE or BLACK) // bit 13-14: white pawn file (from FILE_A to FILE_D) // bit 15-17: white pawn RANK_7 - rank (from RANK_7 - RANK_7 to RANK_7 - RANK_2) - unsigned index(Color us, Square bksq, Square wksq, Square psq) { - return int(wksq) | (bksq << 6) | (us << 12) | (file_of(psq) << 13) | ((RANK_7 - rank_of(psq)) << 15); + unsigned index(Color stm, Square bksq, Square wksq, Square psq) { + return int(wksq) | (bksq << 6) | (stm << 12) | (file_of(psq) << 13) | ((RANK_7 - rank_of(psq)) << 15); } enum Result { @@ -60,25 +59,20 @@ namespace { KPKPosition() = default; explicit KPKPosition(unsigned idx); operator Result() const { return result; } - Result classify(const std::vector& db) - { return us == WHITE ? classify(db) : classify(db); } - - template Result classify(const std::vector& db); + Result classify(const std::vector& db); - Color us; + Color stm; Square ksq[COLOR_NB], psq; Result result; }; } // namespace - -bool Bitbases::probe(Square wksq, Square wpsq, Square bksq, Color us) { +bool Bitbases::probe(Square wksq, Square wpsq, Square bksq, Color stm) { assert(file_of(wpsq) <= FILE_D); - unsigned idx = index(us, bksq, wksq, wpsq); - return KPKBitbase[idx / 32] & (1 << (idx & 0x1F)); + return KPKBitbase[index(stm, bksq, wksq, wpsq)]; } @@ -97,41 +91,40 @@ void Bitbases::init() { for (repeat = idx = 0; idx < MAX_INDEX; ++idx) repeat |= (db[idx] == UNKNOWN && db[idx].classify(db) != UNKNOWN); - // Map 32 results into one KPKBitbase[] entry + // Fill the bitbase with the decisive results for (idx = 0; idx < MAX_INDEX; ++idx) if (db[idx] == WIN) - KPKBitbase[idx / 32] |= 1 << (idx & 0x1F); + KPKBitbase.set(idx); } - namespace { KPKPosition::KPKPosition(unsigned idx) { ksq[WHITE] = Square((idx >> 0) & 0x3F); ksq[BLACK] = Square((idx >> 6) & 0x3F); - us = Color ((idx >> 12) & 0x01); + stm = Color ((idx >> 12) & 0x01); psq = make_square(File((idx >> 13) & 0x3), Rank(RANK_7 - ((idx >> 15) & 0x7))); - // Check if two pieces are on the same square or if a king can be captured + // Invalid if two pieces are on the same square or if a king can be captured if ( distance(ksq[WHITE], ksq[BLACK]) <= 1 || ksq[WHITE] == psq || ksq[BLACK] == psq - || (us == WHITE && (PawnAttacks[WHITE][psq] & ksq[BLACK]))) + || (stm == WHITE && (pawn_attacks_bb(WHITE, psq) & ksq[BLACK]))) result = INVALID; - // Immediate win if a pawn can be promoted without getting captured - else if ( us == WHITE + // Win if the pawn can be promoted without getting captured + else if ( stm == WHITE && rank_of(psq) == RANK_7 - && ksq[us] != psq + NORTH - && ( distance(ksq[~us], psq + NORTH) > 1 - || (PseudoAttacks[KING][ksq[us]] & (psq + NORTH)))) + && ksq[WHITE] != psq + NORTH + && ( distance(ksq[BLACK], psq + NORTH) > 1 + || (distance(ksq[WHITE], psq + NORTH) == 1))) result = WIN; - // Immediate draw if it is a stalemate or a king captures undefended pawn - else if ( us == BLACK - && ( !(PseudoAttacks[KING][ksq[us]] & ~(PseudoAttacks[KING][ksq[~us]] | PawnAttacks[~us][psq])) - || (PseudoAttacks[KING][ksq[us]] & psq & ~PseudoAttacks[KING][ksq[~us]]))) + // Draw if it is stalemate or the black king can capture the pawn + else if ( stm == BLACK + && ( !(attacks_bb(ksq[BLACK]) & ~(attacks_bb(ksq[WHITE]) | pawn_attacks_bb(WHITE, psq))) + || (attacks_bb(ksq[BLACK]) & ~attacks_bb(ksq[WHITE]) & psq))) result = DRAW; // Position will be classified later @@ -139,7 +132,6 @@ namespace { result = UNKNOWN; } - template Result KPKPosition::classify(const std::vector& db) { // White to move: If one move leads to a position classified as WIN, the result @@ -151,30 +143,30 @@ namespace { // of the current position is DRAW. If all moves lead to positions classified // as WIN, the position is classified as WIN, otherwise the current position is // classified as UNKNOWN. - - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); - constexpr Result Good = (Us == WHITE ? WIN : DRAW); - constexpr Result Bad = (Us == WHITE ? DRAW : WIN); + const Result Good = (stm == WHITE ? WIN : DRAW); + const Result Bad = (stm == WHITE ? DRAW : WIN); Result r = INVALID; - Bitboard b = PseudoAttacks[KING][ksq[Us]]; + Bitboard b = attacks_bb(ksq[stm]); while (b) - r |= Us == WHITE ? db[index(Them, ksq[Them] , pop_lsb(&b), psq)] - : db[index(Them, pop_lsb(&b), ksq[Them] , psq)]; + r |= stm == WHITE ? db[index(BLACK, ksq[BLACK], pop_lsb(b), psq)] + : db[index(WHITE, pop_lsb(b), ksq[WHITE], psq)]; - if (Us == WHITE) + if (stm == WHITE) { if (rank_of(psq) < RANK_7) // Single push - r |= db[index(Them, ksq[Them], ksq[Us], psq + NORTH)]; + r |= db[index(BLACK, ksq[BLACK], ksq[WHITE], psq + NORTH)]; if ( rank_of(psq) == RANK_2 // Double push - && psq + NORTH != ksq[Us] - && psq + NORTH != ksq[Them]) - r |= db[index(Them, ksq[Them], ksq[Us], psq + NORTH + NORTH)]; + && psq + NORTH != ksq[WHITE] + && psq + NORTH != ksq[BLACK]) + r |= db[index(BLACK, ksq[BLACK], ksq[WHITE], psq + NORTH + NORTH)]; } return result = r & Good ? Good : r & UNKNOWN ? UNKNOWN : Bad; } } // namespace + +} // namespace Stockfish diff --git a/src/bitboard.cpp b/src/bitboard.cpp index 70114f20fad..fd0ba235cb4 100644 --- a/src/bitboard.cpp +++ b/src/bitboard.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,11 +22,14 @@ #include "bitboard.h" #include "misc.h" +namespace Stockfish { + uint8_t PopCnt16[1 << 16]; uint8_t SquareDistance[SQUARE_NB][SQUARE_NB]; Bitboard SquareBB[SQUARE_NB]; Bitboard LineBB[SQUARE_NB][SQUARE_NB]; +Bitboard BetweenBB[SQUARE_NB][SQUARE_NB]; Bitboard PseudoAttacks[PIECE_TYPE_NB][SQUARE_NB]; Bitboard PawnAttacks[COLOR_NB][SQUARE_NB]; @@ -40,14 +41,23 @@ namespace { Bitboard RookTable[0x19000]; // To store rook attacks Bitboard BishopTable[0x1480]; // To store bishop attacks - void init_magics(Bitboard table[], Magic magics[], Direction directions[]); + void init_magics(PieceType pt, Bitboard table[], Magic magics[]); + +} + +/// safe_destination() returns the bitboard of target square for the given step +/// from the given square. If the step is off the board, returns empty bitboard. + +inline Bitboard safe_destination(Square s, int step) { + Square to = Square(s + step); + return is_ok(to) && distance(s, to) <= 2 ? square_bb(to) : Bitboard(0); } /// Bitboards::pretty() returns an ASCII representation of a bitboard suitable /// to be printed to standard output. Useful for debugging. -const std::string Bitboards::pretty(Bitboard b) { +std::string Bitboards::pretty(Bitboard b) { std::string s = "+---+---+---+---+---+---+---+---+\n"; @@ -56,8 +66,9 @@ const std::string Bitboards::pretty(Bitboard b) { for (File f = FILE_A; f <= FILE_H; ++f) s += b & make_square(f, r) ? "| X " : "| "; - s += "|\n+---+---+---+---+---+---+---+---+\n"; + s += "| " + std::to_string(1 + r) + "\n+---+---+---+---+---+---+---+---+\n"; } + s += " a b c d e f g h\n"; return s; } @@ -69,7 +80,7 @@ const std::string Bitboards::pretty(Bitboard b) { void Bitboards::init() { for (unsigned i = 0; i < (1 << 16); ++i) - PopCnt16[i] = std::bitset<16>(i).count(); + PopCnt16[i] = uint8_t(std::bitset<16>(i).count()); for (Square s = SQ_A1; s <= SQ_H8; ++s) SquareBB[s] = (1ULL << s); @@ -78,65 +89,52 @@ void Bitboards::init() { for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2) SquareDistance[s1][s2] = std::max(distance(s1, s2), distance(s1, s2)); - for (Square s = SQ_A1; s <= SQ_H8; ++s) - { - PawnAttacks[WHITE][s] = pawn_attacks_bb(square_bb(s)); - PawnAttacks[BLACK][s] = pawn_attacks_bb(square_bb(s)); - } + init_magics(ROOK, RookTable, RookMagics); + init_magics(BISHOP, BishopTable, BishopMagics); - // Helper returning the target bitboard of a step from a square - auto landing_square_bb = [&](Square s, int step) + for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) { - Square to = Square(s + step); - return is_ok(to) && distance(s, to) <= 2 ? square_bb(to) : Bitboard(0); - }; + PawnAttacks[WHITE][s1] = pawn_attacks_bb(square_bb(s1)); + PawnAttacks[BLACK][s1] = pawn_attacks_bb(square_bb(s1)); - for (Square s = SQ_A1; s <= SQ_H8; ++s) - { for (int step : {-9, -8, -7, -1, 1, 7, 8, 9} ) - PseudoAttacks[KING][s] |= landing_square_bb(s, step); + PseudoAttacks[KING][s1] |= safe_destination(s1, step); for (int step : {-17, -15, -10, -6, 6, 10, 15, 17} ) - PseudoAttacks[KNIGHT][s] |= landing_square_bb(s, step); - } + PseudoAttacks[KNIGHT][s1] |= safe_destination(s1, step); - Direction RookDirections[] = { NORTH, EAST, SOUTH, WEST }; - Direction BishopDirections[] = { NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST }; - - init_magics(RookTable, RookMagics, RookDirections); - init_magics(BishopTable, BishopMagics, BishopDirections); - - for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) - { PseudoAttacks[QUEEN][s1] = PseudoAttacks[BISHOP][s1] = attacks_bb(s1, 0); PseudoAttacks[QUEEN][s1] |= PseudoAttacks[ ROOK][s1] = attacks_bb< ROOK>(s1, 0); for (PieceType pt : { BISHOP, ROOK }) for (Square s2 = SQ_A1; s2 <= SQ_H8; ++s2) + { if (PseudoAttacks[pt][s1] & s2) - LineBB[s1][s2] = (attacks_bb(pt, s1, 0) & attacks_bb(pt, s2, 0)) | s1 | s2; + { + LineBB[s1][s2] = (attacks_bb(pt, s1, 0) & attacks_bb(pt, s2, 0)) | s1 | s2; + BetweenBB[s1][s2] = (attacks_bb(pt, s1, square_bb(s2)) & attacks_bb(pt, s2, square_bb(s1))); + } + BetweenBB[s1][s2] |= s2; + } } } - namespace { - Bitboard sliding_attack(Direction directions[], Square sq, Bitboard occupied) { + Bitboard sliding_attack(PieceType pt, Square sq, Bitboard occupied) { - Bitboard attack = 0; + Bitboard attacks = 0; + Direction RookDirections[4] = {NORTH, SOUTH, EAST, WEST}; + Direction BishopDirections[4] = {NORTH_EAST, SOUTH_EAST, SOUTH_WEST, NORTH_WEST}; - for (int i = 0; i < 4; ++i) - for (Square s = sq + directions[i]; - is_ok(s) && distance(s, s - directions[i]) == 1; - s += directions[i]) - { - attack |= s; - - if (occupied & s) - break; - } + for (Direction d : (pt == ROOK ? RookDirections : BishopDirections)) + { + Square s = sq; + while (safe_destination(s, d) && !(occupied & s)) + attacks |= (s += d); + } - return attack; + return attacks; } @@ -145,7 +143,7 @@ namespace { // www.chessprogramming.org/Magic_Bitboards. In particular, here we use the so // called "fancy" approach. - void init_magics(Bitboard table[], Magic magics[], Direction directions[]) { + void init_magics(PieceType pt, Bitboard table[], Magic magics[]) { // Optimal PRNG seeds to pick the correct magics in the shortest time int seeds[][RANK_NB] = { { 8977, 44560, 54343, 38998, 5731, 95205, 104912, 17020 }, @@ -165,7 +163,7 @@ namespace { // the number of 1s of the mask. Hence we deduce the size of the shift to // apply to the 64 or 32 bits word to get the index. Magic& m = magics[s]; - m.mask = sliding_attack(directions, s, 0) & ~edges; + m.mask = sliding_attack(pt, s, 0) & ~edges; m.shift = (Is64Bit ? 64 : 32) - popcount(m.mask); // Set the offset for the attacks table of the square. We have individual @@ -177,7 +175,7 @@ namespace { b = size = 0; do { occupancy[size] = b; - reference[size] = sliding_attack(directions, s, b); + reference[size] = sliding_attack(pt, s, b); if (HasPext) m.attacks[pext(b, m.mask)] = reference[size]; @@ -220,3 +218,5 @@ namespace { } } } + +} // namespace Stockfish diff --git a/src/bitboard.h b/src/bitboard.h index 440de1ea1ee..2b6e2a6920c 100644 --- a/src/bitboard.h +++ b/src/bitboard.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,19 +23,21 @@ #include "types.h" +namespace Stockfish { + namespace Bitbases { void init(); bool probe(Square wksq, Square wpsq, Square bksq, Color us); -} +} // namespace Stockfish::Bitbases namespace Bitboards { void init(); -const std::string pretty(Bitboard b); +std::string pretty(Bitboard b); -} +} // namespace Stockfish::Bitboards constexpr Bitboard AllSquares = ~Bitboard(0); constexpr Bitboard DarkSquares = 0xAA55AA55AA55AA55ULL; @@ -75,6 +75,7 @@ extern uint8_t PopCnt16[1 << 16]; extern uint8_t SquareDistance[SQUARE_NB][SQUARE_NB]; extern Bitboard SquareBB[SQUARE_NB]; +extern Bitboard BetweenBB[SQUARE_NB][SQUARE_NB]; extern Bitboard LineBB[SQUARE_NB][SQUARE_NB]; extern Bitboard PseudoAttacks[PIECE_TYPE_NB][SQUARE_NB]; extern Bitboard PawnAttacks[COLOR_NB][SQUARE_NB]; @@ -106,10 +107,11 @@ extern Magic RookMagics[SQUARE_NB]; extern Magic BishopMagics[SQUARE_NB]; inline Bitboard square_bb(Square s) { - assert(s >= SQ_A1 && s <= SQ_H8); + assert(is_ok(s)); return SquareBB[s]; } + /// Overloads of bitwise operators between a Bitboard and a Square for testing /// whether a given bit is set in a bitboard, and for setting and clearing bits. @@ -123,38 +125,39 @@ inline Bitboard operator&(Square s, Bitboard b) { return b & s; } inline Bitboard operator|(Square s, Bitboard b) { return b | s; } inline Bitboard operator^(Square s, Bitboard b) { return b ^ s; } -inline Bitboard operator|(Square s, Square s2) { return square_bb(s) | square_bb(s2); } +inline Bitboard operator|(Square s1, Square s2) { return square_bb(s1) | s2; } constexpr bool more_than_one(Bitboard b) { return b & (b - 1); } -inline bool opposite_colors(Square s1, Square s2) { - return bool(DarkSquares & s1) != bool(DarkSquares & s2); + +constexpr bool opposite_colors(Square s1, Square s2) { + return (s1 + rank_of(s1) + s2 + rank_of(s2)) & 1; } /// rank_bb() and file_bb() return a bitboard representing all the squares on /// the given file or rank. -inline Bitboard rank_bb(Rank r) { +constexpr Bitboard rank_bb(Rank r) { return Rank1BB << (8 * r); } -inline Bitboard rank_bb(Square s) { +constexpr Bitboard rank_bb(Square s) { return rank_bb(rank_of(s)); } -inline Bitboard file_bb(File f) { +constexpr Bitboard file_bb(File f) { return FileABB << f; } -inline Bitboard file_bb(Square s) { +constexpr Bitboard file_bb(Square s) { return file_bb(file_of(s)); } -/// shift() moves a bitboard one step along direction D +/// shift() moves a bitboard one or two steps as specified by the direction D template constexpr Bitboard shift(Bitboard b) { @@ -176,6 +179,12 @@ constexpr Bitboard pawn_attacks_bb(Bitboard b) { : shift(b) | shift(b); } +inline Bitboard pawn_attacks_bb(Color c, Square s) { + + assert(is_ok(s)); + return PawnAttacks[c][s]; +} + /// pawn_double_attacks_bb() returns the squares doubly attacked by pawns of the /// given color from the squares in the given bitboard. @@ -188,45 +197,65 @@ constexpr Bitboard pawn_double_attacks_bb(Bitboard b) { /// adjacent_files_bb() returns a bitboard representing all the squares on the -/// adjacent files of the given one. +/// adjacent files of a given square. -inline Bitboard adjacent_files_bb(Square s) { +constexpr Bitboard adjacent_files_bb(Square s) { return shift(file_bb(s)) | shift(file_bb(s)); } -/// between_bb() returns squares that are linearly between the given squares -/// If the given squares are not on a same file/rank/diagonal, return 0. +/// line_bb() returns a bitboard representing an entire line (from board edge +/// to board edge) that intersects the two given squares. If the given squares +/// are not on a same file/rank/diagonal, the function returns 0. For instance, +/// line_bb(SQ_C4, SQ_F7) will return a bitboard with the A2-G8 diagonal. + +inline Bitboard line_bb(Square s1, Square s2) { + + assert(is_ok(s1) && is_ok(s2)); + + return LineBB[s1][s2]; +} + + +/// between_bb(s1, s2) returns a bitboard representing the squares in the semi-open +/// segment between the squares s1 and s2 (excluding s1 but including s2). If the +/// given squares are not on a same file/rank/diagonal, it returns s2. For instance, +/// between_bb(SQ_C4, SQ_F7) will return a bitboard with squares D5, E6 and F7, but +/// between_bb(SQ_E6, SQ_F8) will return a bitboard with the square F8. This trick +/// allows to generate non-king evasion moves faster: the defending piece must either +/// interpose itself to cover the check or capture the checking piece. inline Bitboard between_bb(Square s1, Square s2) { - return LineBB[s1][s2] & ( (AllSquares << (s1 + (s1 < s2))) - ^(AllSquares << (s2 + !(s1 < s2)))); + + assert(is_ok(s1) && is_ok(s2)); + + return BetweenBB[s1][s2]; } -/// forward_ranks_bb() returns a bitboard representing the squares on the ranks -/// in front of the given one, from the point of view of the given color. For instance, +/// forward_ranks_bb() returns a bitboard representing the squares on the ranks in +/// front of the given one, from the point of view of the given color. For instance, /// forward_ranks_bb(BLACK, SQ_D3) will return the 16 squares on ranks 1 and 2. -inline Bitboard forward_ranks_bb(Color c, Square s) { - return c == WHITE ? ~Rank1BB << 8 * (rank_of(s) - RANK_1) - : ~Rank8BB >> 8 * (RANK_8 - rank_of(s)); +constexpr Bitboard forward_ranks_bb(Color c, Square s) { + return c == WHITE ? ~Rank1BB << 8 * relative_rank(WHITE, s) + : ~Rank8BB >> 8 * relative_rank(BLACK, s); } /// forward_file_bb() returns a bitboard representing all the squares along the /// line in front of the given one, from the point of view of the given color. -inline Bitboard forward_file_bb(Color c, Square s) { +constexpr Bitboard forward_file_bb(Color c, Square s) { return forward_ranks_bb(c, s) & file_bb(s); } /// pawn_attack_span() returns a bitboard representing all the squares that can -/// be attacked by a pawn of the given color when it moves along its file, -/// starting from the given square. +/// be attacked by a pawn of the given color when it moves along its file, starting +/// from the given square. -inline Bitboard pawn_attack_span(Color c, Square s) { +constexpr Bitboard pawn_attack_span(Color c, Square s) { return forward_ranks_bb(c, s) & adjacent_files_bb(s); } @@ -234,8 +263,8 @@ inline Bitboard pawn_attack_span(Color c, Square s) { /// passed_pawn_span() returns a bitboard which can be used to test if a pawn of /// the given color and on the given square is a passed pawn. -inline Bitboard passed_pawn_span(Color c, Square s) { - return forward_ranks_bb(c, s) & (adjacent_files_bb(s) | file_bb(s)); +constexpr Bitboard passed_pawn_span(Color c, Square s) { + return pawn_attack_span(c, s) | forward_file_bb(c, s); } @@ -243,7 +272,7 @@ inline Bitboard passed_pawn_span(Color c, Square s) { /// straight or on a diagonal line. inline bool aligned(Square s1, Square s2, Square s3) { - return LineBB[s1][s2] & s3; + return line_bb(s1, s2) & s3; } @@ -255,23 +284,43 @@ template<> inline int distance(Square x, Square y) { return std::abs(file_ template<> inline int distance(Square x, Square y) { return std::abs(rank_of(x) - rank_of(y)); } template<> inline int distance(Square x, Square y) { return SquareDistance[x][y]; } -template constexpr const T& clamp(const T& v, const T& lo, const T& hi) { - return v < lo ? lo : v > hi ? hi : v; +inline int edge_distance(File f) { return std::min(f, File(FILE_H - f)); } +inline int edge_distance(Rank r) { return std::min(r, Rank(RANK_8 - r)); } + + +/// attacks_bb(Square) returns the pseudo attacks of the give piece type +/// assuming an empty board. + +template +inline Bitboard attacks_bb(Square s) { + + assert((Pt != PAWN) && (is_ok(s))); + + return PseudoAttacks[Pt][s]; } -/// attacks_bb() returns a bitboard representing all the squares attacked by a -/// piece of type Pt (bishop or rook) placed on 's'. + +/// attacks_bb(Square, Bitboard) returns the attacks by the given piece +/// assuming the board is occupied according to the passed Bitboard. +/// Sliding piece attacks do not continue passed an occupied square. template inline Bitboard attacks_bb(Square s, Bitboard occupied) { - const Magic& m = Pt == ROOK ? RookMagics[s] : BishopMagics[s]; - return m.attacks[m.index(occupied)]; + assert((Pt != PAWN) && (is_ok(s))); + + switch (Pt) + { + case BISHOP: return BishopMagics[s].attacks[BishopMagics[s].index(occupied)]; + case ROOK : return RookMagics[s].attacks[ RookMagics[s].index(occupied)]; + case QUEEN : return attacks_bb(s, occupied) | attacks_bb(s, occupied); + default : return PseudoAttacks[Pt][s]; + } } inline Bitboard attacks_bb(PieceType pt, Square s, Bitboard occupied) { - assert(pt != PAWN); + assert((pt != PAWN) && (is_ok(s))); switch (pt) { @@ -372,19 +421,31 @@ inline Square msb(Bitboard b) { #endif +/// least_significant_square_bb() returns the bitboard of the least significant +/// square of a non-zero bitboard. It is equivalent to square_bb(lsb(bb)). + +inline Bitboard least_significant_square_bb(Bitboard b) { + assert(b); + return b & -b; +} /// pop_lsb() finds and clears the least significant bit in a non-zero bitboard -inline Square pop_lsb(Bitboard* b) { - const Square s = lsb(*b); - *b &= *b - 1; +inline Square pop_lsb(Bitboard& b) { + assert(b); + const Square s = lsb(b); + b &= b - 1; return s; } -/// frontmost_sq() returns the most advanced square for the given color +/// frontmost_sq() returns the most advanced square for the given color, +/// requires a non-zero bitboard. inline Square frontmost_sq(Color c, Bitboard b) { + assert(b); return c == WHITE ? msb(b) : lsb(b); } +} // namespace Stockfish + #endif // #ifndef BITBOARD_H_INCLUDED diff --git a/src/endgame.cpp b/src/endgame.cpp index 2ed6ebc27b7..e773e7a9119 100644 --- a/src/endgame.cpp +++ b/src/endgame.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,42 +22,27 @@ #include "endgame.h" #include "movegen.h" -using std::string; +namespace Stockfish { namespace { - // Table used to drive the king towards the edge of the board + // Used to drive the king towards the edge of the board // in KX vs K and KQ vs KR endgames. - constexpr int PushToEdges[SQUARE_NB] = { - 100, 90, 80, 70, 70, 80, 90, 100, - 90, 70, 60, 50, 50, 60, 70, 90, - 80, 60, 40, 30, 30, 40, 60, 80, - 70, 50, 30, 20, 20, 30, 50, 70, - 70, 50, 30, 20, 20, 30, 50, 70, - 80, 60, 40, 30, 30, 40, 60, 80, - 90, 70, 60, 50, 50, 60, 70, 90, - 100, 90, 80, 70, 70, 80, 90, 100 - }; - - // Table used to drive the king towards a corner square of the - // right color in KBN vs K endgames. - constexpr int PushToCorners[SQUARE_NB] = { - 6400, 6080, 5760, 5440, 5120, 4800, 4480, 4160, - 6080, 5760, 5440, 5120, 4800, 4480, 4160, 4480, - 5760, 5440, 4960, 4480, 4480, 4000, 4480, 4800, - 5440, 5120, 4480, 3840, 3520, 4480, 4800, 5120, - 5120, 4800, 4480, 3520, 3840, 4480, 5120, 5440, - 4800, 4480, 4000, 4480, 4480, 4960, 5440, 5760, - 4480, 4160, 4480, 4800, 5120, 5440, 5760, 6080, - 4160, 4480, 4800, 5120, 5440, 5760, 6080, 6400 - }; - - // Tables used to drive a piece towards or away from another piece - constexpr int PushClose[8] = { 0, 0, 100, 80, 60, 40, 20, 10 }; - constexpr int PushAway [8] = { 0, 5, 20, 40, 60, 80, 90, 100 }; - - // Pawn Rank based scaling factors used in KRPPKRP endgame - constexpr int KRPPKRPScaleFactors[RANK_NB] = { 0, 9, 10, 14, 21, 44, 0, 0 }; + // Values range from 27 (center squares) to 90 (in the corners) + inline int push_to_edge(Square s) { + int rd = edge_distance(rank_of(s)), fd = edge_distance(file_of(s)); + return 90 - (7 * fd * fd / 2 + 7 * rd * rd / 2); + } + + // Used to drive the king towards A1H8 corners in KBN vs K endgames. + // Values range from 0 on A8H1 diagonal to 7 in A1H8 corners + inline int push_to_corner(Square s) { + return abs(7 - rank_of(s) - file_of(s)); + } + + // Drive a piece close to or away from another piece + inline int push_close(Square s1, Square s2) { return 140 - 20 * distance(s1, s2); } + inline int push_away(Square s1, Square s2) { return 120 - push_close(s1, s2); } #ifndef NDEBUG bool verify_material(const Position& pos, Color c, Value npm, int pawnsCnt) { @@ -74,9 +57,9 @@ namespace { assert(pos.count(strongSide) == 1); if (file_of(pos.square(strongSide)) >= FILE_E) - sq = Square(int(sq) ^ 7); // Mirror SQ_H1 -> SQ_A1 + sq = flip_file(sq); - return strongSide == WHITE ? sq : ~sq; + return strongSide == WHITE ? sq : flip_rank(sq); } } // namespace @@ -98,8 +81,6 @@ namespace Endgames { add("KQKR"); add("KNNKP"); - add("KNPK"); - add("KNPKB"); add("KRPKR"); add("KRPKB"); add("KBPKB"); @@ -124,20 +105,20 @@ Value Endgame::operator()(const Position& pos) const { if (pos.side_to_move() == weakSide && !MoveList(pos).size()) return VALUE_DRAW; - Square winnerKSq = pos.square(strongSide); - Square loserKSq = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + Square weakKing = pos.square(weakSide); Value result = pos.non_pawn_material(strongSide) + pos.count(strongSide) * PawnValueEg - + PushToEdges[loserKSq] - + PushClose[distance(winnerKSq, loserKSq)]; + + push_to_edge(weakKing) + + push_close(strongKing, weakKing); if ( pos.count(strongSide) || pos.count(strongSide) ||(pos.count(strongSide) && pos.count(strongSide)) || ( (pos.pieces(strongSide, BISHOP) & ~DarkSquares) && (pos.pieces(strongSide, BISHOP) & DarkSquares))) - result = std::min(result + VALUE_KNOWN_WIN, VALUE_MATE_IN_MAX_PLY - 1); + result = std::min(result + VALUE_KNOWN_WIN, VALUE_TB_WIN_IN_MAX_PLY - 1); return strongSide == pos.side_to_move() ? result : -result; } @@ -151,18 +132,18 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, KnightValueMg + BishopValueMg, 0)); assert(verify_material(pos, weakSide, VALUE_ZERO, 0)); - Square winnerKSq = pos.square(strongSide); - Square loserKSq = pos.square(weakSide); - Square bishopSq = pos.square(strongSide); + Square strongKing = pos.square(strongSide); + Square strongBishop = pos.square(strongSide); + Square weakKing = pos.square(weakSide); // If our bishop does not attack A1/H8, we flip the enemy king square // to drive to opposite corners (A8/H1). - Value result = VALUE_KNOWN_WIN - + PushClose[distance(winnerKSq, loserKSq)] - + PushToCorners[opposite_colors(bishopSq, SQ_A1) ? ~loserKSq : loserKSq]; + Value result = (VALUE_KNOWN_WIN + 3520) + + push_close(strongKing, weakKing) + + 420 * push_to_corner(opposite_colors(strongBishop, SQ_A1) ? flip_file(weakKing) : weakKing); - assert(abs(result) < VALUE_MATE_IN_MAX_PLY); + assert(abs(result) < VALUE_TB_WIN_IN_MAX_PLY); return strongSide == pos.side_to_move() ? result : -result; } @@ -175,16 +156,16 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, weakSide, VALUE_ZERO, 0)); // Assume strongSide is white and the pawn is on files A-D - Square wksq = normalize(pos, strongSide, pos.square(strongSide)); - Square bksq = normalize(pos, strongSide, pos.square(weakSide)); - Square psq = normalize(pos, strongSide, pos.square(strongSide)); + Square strongKing = normalize(pos, strongSide, pos.square(strongSide)); + Square strongPawn = normalize(pos, strongSide, pos.square(strongSide)); + Square weakKing = normalize(pos, strongSide, pos.square(weakSide)); Color us = strongSide == pos.side_to_move() ? WHITE : BLACK; - if (!Bitbases::probe(wksq, psq, bksq, us)) + if (!Bitbases::probe(strongKing, strongPawn, weakKing, us)) return VALUE_DRAW; - Value result = VALUE_KNOWN_WIN + PawnValueEg + Value(rank_of(psq)); + Value result = VALUE_KNOWN_WIN + PawnValueEg + Value(rank_of(strongPawn)); return strongSide == pos.side_to_move() ? result : -result; } @@ -200,36 +181,35 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, RookValueMg, 0)); assert(verify_material(pos, weakSide, VALUE_ZERO, 1)); - Square wksq = relative_square(strongSide, pos.square(strongSide)); - Square bksq = relative_square(strongSide, pos.square(weakSide)); - Square rsq = relative_square(strongSide, pos.square(strongSide)); - Square psq = relative_square(strongSide, pos.square(weakSide)); - - Square queeningSq = make_square(file_of(psq), RANK_1); + Square strongKing = pos.square(strongSide); + Square weakKing = pos.square(weakSide); + Square strongRook = pos.square(strongSide); + Square weakPawn = pos.square(weakSide); + Square queeningSquare = make_square(file_of(weakPawn), relative_rank(weakSide, RANK_8)); Value result; // If the stronger side's king is in front of the pawn, it's a win - if (forward_file_bb(WHITE, wksq) & psq) - result = RookValueEg - distance(wksq, psq); + if (forward_file_bb(strongSide, strongKing) & weakPawn) + result = RookValueEg - distance(strongKing, weakPawn); // If the weaker side's king is too far from the pawn and the rook, // it's a win. - else if ( distance(bksq, psq) >= 3 + (pos.side_to_move() == weakSide) - && distance(bksq, rsq) >= 3) - result = RookValueEg - distance(wksq, psq); + else if ( distance(weakKing, weakPawn) >= 3 + (pos.side_to_move() == weakSide) + && distance(weakKing, strongRook) >= 3) + result = RookValueEg - distance(strongKing, weakPawn); // If the pawn is far advanced and supported by the defending king, // the position is drawish - else if ( rank_of(bksq) <= RANK_3 - && distance(bksq, psq) == 1 - && rank_of(wksq) >= RANK_4 - && distance(wksq, psq) > 2 + (pos.side_to_move() == strongSide)) - result = Value(80) - 8 * distance(wksq, psq); + else if ( relative_rank(strongSide, weakKing) <= RANK_3 + && distance(weakKing, weakPawn) == 1 + && relative_rank(strongSide, strongKing) >= RANK_4 + && distance(strongKing, weakPawn) > 2 + (pos.side_to_move() == strongSide)) + result = Value(80) - 8 * distance(strongKing, weakPawn); else - result = Value(200) - 8 * ( distance(wksq, psq + SOUTH) - - distance(bksq, psq + SOUTH) - - distance(psq, queeningSq)); + result = Value(200) - 8 * ( distance(strongKing, weakPawn + pawn_push(weakSide)) + - distance(weakKing, weakPawn + pawn_push(weakSide)) + - distance(weakPawn, queeningSquare)); return strongSide == pos.side_to_move() ? result : -result; } @@ -243,7 +223,7 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, RookValueMg, 0)); assert(verify_material(pos, weakSide, BishopValueMg, 0)); - Value result = Value(PushToEdges[pos.square(weakSide)]); + Value result = Value(push_to_edge(pos.square(weakSide))); return strongSide == pos.side_to_move() ? result : -result; } @@ -256,9 +236,9 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, RookValueMg, 0)); assert(verify_material(pos, weakSide, KnightValueMg, 0)); - Square bksq = pos.square(weakSide); - Square bnsq = pos.square(weakSide); - Value result = Value(PushToEdges[bksq] + PushAway[distance(bksq, bnsq)]); + Square weakKing = pos.square(weakSide); + Square weakKnight = pos.square(weakSide); + Value result = Value(push_to_edge(weakKing) + push_away(weakKing, weakKnight)); return strongSide == pos.side_to_move() ? result : -result; } @@ -273,22 +253,22 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, QueenValueMg, 0)); assert(verify_material(pos, weakSide, VALUE_ZERO, 1)); - Square winnerKSq = pos.square(strongSide); - Square loserKSq = pos.square(weakSide); - Square pawnSq = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + Square weakKing = pos.square(weakSide); + Square weakPawn = pos.square(weakSide); - Value result = Value(PushClose[distance(winnerKSq, loserKSq)]); + Value result = Value(push_close(strongKing, weakKing)); - if ( relative_rank(weakSide, pawnSq) != RANK_7 - || distance(loserKSq, pawnSq) != 1 - || !((FileABB | FileCBB | FileFBB | FileHBB) & pawnSq)) + if ( relative_rank(weakSide, weakPawn) != RANK_7 + || distance(weakKing, weakPawn) != 1 + || ((FileBBB | FileDBB | FileEBB | FileGBB) & weakPawn)) result += QueenValueEg - PawnValueEg; return strongSide == pos.side_to_move() ? result : -result; } -/// KQ vs KR. This is almost identical to KX vs K: We give the attacking +/// KQ vs KR. This is almost identical to KX vs K: we give the attacking /// king a bonus for having the kings close together, and for forcing the /// defending king towards the edge. If we also take care to avoid null move for /// the defending side in the search, this is usually sufficient to win KQ vs KR. @@ -298,28 +278,32 @@ Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, QueenValueMg, 0)); assert(verify_material(pos, weakSide, RookValueMg, 0)); - Square winnerKSq = pos.square(strongSide); - Square loserKSq = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + Square weakKing = pos.square(weakSide); Value result = QueenValueEg - RookValueEg - + PushToEdges[loserKSq] - + PushClose[distance(winnerKSq, loserKSq)]; + + push_to_edge(weakKing) + + push_close(strongKing, weakKing); return strongSide == pos.side_to_move() ? result : -result; } -/// KNN vs KP. Simply push the opposing king to the corner +/// KNN vs KP. Very drawish, but there are some mate opportunities if we can +/// press the weakSide King to a corner before the pawn advances too much. template<> Value Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, 2 * KnightValueMg, 0)); assert(verify_material(pos, weakSide, VALUE_ZERO, 1)); - Value result = 2 * KnightValueEg - - PawnValueEg - + PushToEdges[pos.square(weakSide)]; + Square weakKing = pos.square(weakSide); + Square weakPawn = pos.square(weakSide); + + Value result = PawnValueEg + + 2 * push_to_edge(weakKing) + - 10 * relative_rank(weakSide, weakPawn); return strongSide == pos.side_to_move() ? result : -result; } @@ -342,51 +326,47 @@ ScaleFactor Endgame::operator()(const Position& pos) const { // No assertions about the material of weakSide, because we want draws to // be detected even when the weaker side has some pawns. - Bitboard pawns = pos.pieces(strongSide, PAWN); - File pawnsFile = file_of(lsb(pawns)); + Bitboard strongPawns = pos.pieces(strongSide, PAWN); + Bitboard allPawns = pos.pieces(PAWN); - // All pawns are on a single rook file? - if ( (pawnsFile == FILE_A || pawnsFile == FILE_H) - && !(pawns & ~file_bb(pawnsFile))) + Square strongBishop = pos.square(strongSide); + Square weakKing = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + + // All strongSide pawns are on a single rook file? + if (!(strongPawns & ~FileABB) || !(strongPawns & ~FileHBB)) { - Square bishopSq = pos.square(strongSide); - Square queeningSq = relative_square(strongSide, make_square(pawnsFile, RANK_8)); - Square kingSq = pos.square(weakSide); + Square queeningSquare = relative_square(strongSide, make_square(file_of(lsb(strongPawns)), RANK_8)); - if ( opposite_colors(queeningSq, bishopSq) - && distance(queeningSq, kingSq) <= 1) + if ( opposite_colors(queeningSquare, strongBishop) + && distance(queeningSquare, weakKing) <= 1) return SCALE_FACTOR_DRAW; } // If all the pawns are on the same B or G file, then it's potentially a draw - if ( (pawnsFile == FILE_B || pawnsFile == FILE_G) - && !(pos.pieces(PAWN) & ~file_bb(pawnsFile)) + if ((!(allPawns & ~FileBBB) || !(allPawns & ~FileGBB)) && pos.non_pawn_material(weakSide) == 0 && pos.count(weakSide) >= 1) { - // Get weakSide pawn that is closest to the home rank - Square weakPawnSq = frontmost_sq(strongSide, pos.pieces(weakSide, PAWN)); - - Square strongKingSq = pos.square(strongSide); - Square weakKingSq = pos.square(weakSide); - Square bishopSq = pos.square(strongSide); + // Get the least advanced weakSide pawn + Square weakPawn = frontmost_sq(strongSide, pos.pieces(weakSide, PAWN)); // There's potential for a draw if our pawn is blocked on the 7th rank, - // the bishop cannot attack it or they only have one pawn left - if ( relative_rank(strongSide, weakPawnSq) == RANK_7 - && (pos.pieces(strongSide, PAWN) & (weakPawnSq + pawn_push(weakSide))) - && (opposite_colors(bishopSq, weakPawnSq) || pos.count(strongSide) == 1)) + // the bishop cannot attack it or they only have one pawn left. + if ( relative_rank(strongSide, weakPawn) == RANK_7 + && (strongPawns & (weakPawn + pawn_push(weakSide))) + && (opposite_colors(strongBishop, weakPawn) || !more_than_one(strongPawns))) { - int strongKingDist = distance(weakPawnSq, strongKingSq); - int weakKingDist = distance(weakPawnSq, weakKingSq); + int strongKingDist = distance(weakPawn, strongKing); + int weakKingDist = distance(weakPawn, weakKing); // It's a draw if the weak king is on its back two ranks, within 2 // squares of the blocking pawn and the strong king is not // closer. (I think this rule only fails in practically // unreachable positions such as 5k1K/6p1/6P1/8/8/3B4/8/8 w // and positions where qsearch will immediately correct the - // problem such as 8/4k1p1/6P1/1K6/3B4/8/8/8 w) - if ( relative_rank(strongSide, weakKingSq) >= RANK_7 + // problem such as 8/4k1p1/6P1/1K6/3B4/8/8/8 w). + if ( relative_rank(strongSide, weakKing) >= RANK_7 && weakKingDist <= 2 && weakKingDist <= strongKingDist) return SCALE_FACTOR_DRAW; @@ -406,15 +386,16 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(pos.count(weakSide) == 1); assert(pos.count(weakSide) >= 1); - Square kingSq = pos.square(weakSide); - Square rsq = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + Square weakKing = pos.square(weakSide); + Square weakRook = pos.square(weakSide); - if ( relative_rank(weakSide, kingSq) <= RANK_2 - && relative_rank(weakSide, pos.square(strongSide)) >= RANK_4 - && relative_rank(weakSide, rsq) == RANK_3 + if ( relative_rank(weakSide, weakKing) <= RANK_2 + && relative_rank(weakSide, strongKing) >= RANK_4 + && relative_rank(weakSide, weakRook) == RANK_3 && ( pos.pieces(weakSide, PAWN) - & pos.attacks_from(kingSq) - & pos.attacks_from(rsq, strongSide))) + & attacks_bb(weakKing) + & pawn_attacks_bb(strongSide, weakRook))) return SCALE_FACTOR_DRAW; return SCALE_FACTOR_NONE; @@ -434,89 +415,89 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, weakSide, RookValueMg, 0)); // Assume strongSide is white and the pawn is on files A-D - Square wksq = normalize(pos, strongSide, pos.square(strongSide)); - Square bksq = normalize(pos, strongSide, pos.square(weakSide)); - Square wrsq = normalize(pos, strongSide, pos.square(strongSide)); - Square wpsq = normalize(pos, strongSide, pos.square(strongSide)); - Square brsq = normalize(pos, strongSide, pos.square(weakSide)); - - File f = file_of(wpsq); - Rank r = rank_of(wpsq); - Square queeningSq = make_square(f, RANK_8); + Square strongKing = normalize(pos, strongSide, pos.square(strongSide)); + Square strongRook = normalize(pos, strongSide, pos.square(strongSide)); + Square strongPawn = normalize(pos, strongSide, pos.square(strongSide)); + Square weakKing = normalize(pos, strongSide, pos.square(weakSide)); + Square weakRook = normalize(pos, strongSide, pos.square(weakSide)); + + File pawnFile = file_of(strongPawn); + Rank pawnRank = rank_of(strongPawn); + Square queeningSquare = make_square(pawnFile, RANK_8); int tempo = (pos.side_to_move() == strongSide); // If the pawn is not too far advanced and the defending king defends the // queening square, use the third-rank defence. - if ( r <= RANK_5 - && distance(bksq, queeningSq) <= 1 - && wksq <= SQ_H5 - && (rank_of(brsq) == RANK_6 || (r <= RANK_3 && rank_of(wrsq) != RANK_6))) + if ( pawnRank <= RANK_5 + && distance(weakKing, queeningSquare) <= 1 + && strongKing <= SQ_H5 + && (rank_of(weakRook) == RANK_6 || (pawnRank <= RANK_3 && rank_of(strongRook) != RANK_6))) return SCALE_FACTOR_DRAW; // The defending side saves a draw by checking from behind in case the pawn // has advanced to the 6th rank with the king behind. - if ( r == RANK_6 - && distance(bksq, queeningSq) <= 1 - && rank_of(wksq) + tempo <= RANK_6 - && (rank_of(brsq) == RANK_1 || (!tempo && distance(brsq, wpsq) >= 3))) + if ( pawnRank == RANK_6 + && distance(weakKing, queeningSquare) <= 1 + && rank_of(strongKing) + tempo <= RANK_6 + && (rank_of(weakRook) == RANK_1 || (!tempo && distance(weakRook, strongPawn) >= 3))) return SCALE_FACTOR_DRAW; - if ( r >= RANK_6 - && bksq == queeningSq - && rank_of(brsq) == RANK_1 - && (!tempo || distance(wksq, wpsq) >= 2)) + if ( pawnRank >= RANK_6 + && weakKing == queeningSquare + && rank_of(weakRook) == RANK_1 + && (!tempo || distance(strongKing, strongPawn) >= 2)) return SCALE_FACTOR_DRAW; // White pawn on a7 and rook on a8 is a draw if black's king is on g7 or h7 // and the black rook is behind the pawn. - if ( wpsq == SQ_A7 - && wrsq == SQ_A8 - && (bksq == SQ_H7 || bksq == SQ_G7) - && file_of(brsq) == FILE_A - && (rank_of(brsq) <= RANK_3 || file_of(wksq) >= FILE_D || rank_of(wksq) <= RANK_5)) + if ( strongPawn == SQ_A7 + && strongRook == SQ_A8 + && (weakKing == SQ_H7 || weakKing == SQ_G7) + && file_of(weakRook) == FILE_A + && (rank_of(weakRook) <= RANK_3 || file_of(strongKing) >= FILE_D || rank_of(strongKing) <= RANK_5)) return SCALE_FACTOR_DRAW; // If the defending king blocks the pawn and the attacking king is too far // away, it's a draw. - if ( r <= RANK_5 - && bksq == wpsq + NORTH - && distance(wksq, wpsq) - tempo >= 2 - && distance(wksq, brsq) - tempo >= 2) + if ( pawnRank <= RANK_5 + && weakKing == strongPawn + NORTH + && distance(strongKing, strongPawn) - tempo >= 2 + && distance(strongKing, weakRook) - tempo >= 2) return SCALE_FACTOR_DRAW; // Pawn on the 7th rank supported by the rook from behind usually wins if the // attacking king is closer to the queening square than the defending king, // and the defending king cannot gain tempi by threatening the attacking rook. - if ( r == RANK_7 - && f != FILE_A - && file_of(wrsq) == f - && wrsq != queeningSq - && (distance(wksq, queeningSq) < distance(bksq, queeningSq) - 2 + tempo) - && (distance(wksq, queeningSq) < distance(bksq, wrsq) + tempo)) - return ScaleFactor(SCALE_FACTOR_MAX - 2 * distance(wksq, queeningSq)); + if ( pawnRank == RANK_7 + && pawnFile != FILE_A + && file_of(strongRook) == pawnFile + && strongRook != queeningSquare + && (distance(strongKing, queeningSquare) < distance(weakKing, queeningSquare) - 2 + tempo) + && (distance(strongKing, queeningSquare) < distance(weakKing, strongRook) + tempo)) + return ScaleFactor(SCALE_FACTOR_MAX - 2 * distance(strongKing, queeningSquare)); // Similar to the above, but with the pawn further back - if ( f != FILE_A - && file_of(wrsq) == f - && wrsq < wpsq - && (distance(wksq, queeningSq) < distance(bksq, queeningSq) - 2 + tempo) - && (distance(wksq, wpsq + NORTH) < distance(bksq, wpsq + NORTH) - 2 + tempo) - && ( distance(bksq, wrsq) + tempo >= 3 - || ( distance(wksq, queeningSq) < distance(bksq, wrsq) + tempo - && (distance(wksq, wpsq + NORTH) < distance(bksq, wrsq) + tempo)))) + if ( pawnFile != FILE_A + && file_of(strongRook) == pawnFile + && strongRook < strongPawn + && (distance(strongKing, queeningSquare) < distance(weakKing, queeningSquare) - 2 + tempo) + && (distance(strongKing, strongPawn + NORTH) < distance(weakKing, strongPawn + NORTH) - 2 + tempo) + && ( distance(weakKing, strongRook) + tempo >= 3 + || ( distance(strongKing, queeningSquare) < distance(weakKing, strongRook) + tempo + && (distance(strongKing, strongPawn + NORTH) < distance(weakKing, strongPawn) + tempo)))) return ScaleFactor( SCALE_FACTOR_MAX - - 8 * distance(wpsq, queeningSq) - - 2 * distance(wksq, queeningSq)); + - 8 * distance(strongPawn, queeningSquare) + - 2 * distance(strongKing, queeningSquare)); // If the pawn is not far advanced and the defending king is somewhere in // the pawn's path, it's probably a draw. - if (r <= RANK_4 && bksq > wpsq) + if (pawnRank <= RANK_4 && weakKing > strongPawn) { - if (file_of(bksq) == file_of(wpsq)) + if (file_of(weakKing) == file_of(strongPawn)) return ScaleFactor(10); - if ( distance(bksq, wpsq) == 1 - && distance(wksq, bksq) > 2) - return ScaleFactor(24 - 2 * distance(wksq, bksq)); + if ( distance(weakKing, strongPawn) == 1 + && distance(strongKing, weakKing) > 2) + return ScaleFactor(24 - 2 * distance(strongKing, weakKing)); } return SCALE_FACTOR_NONE; } @@ -530,10 +511,11 @@ ScaleFactor Endgame::operator()(const Position& pos) const { // Test for a rook pawn if (pos.pieces(PAWN) & (FileABB | FileHBB)) { - Square ksq = pos.square(weakSide); - Square bsq = pos.square(weakSide); - Square psq = pos.square(strongSide); - Rank rk = relative_rank(strongSide, psq); + Square weakKing = pos.square(weakSide); + Square weakBishop = pos.square(weakSide); + Square strongKing = pos.square(strongSide); + Square strongPawn = pos.square(strongSide); + Rank pawnRank = relative_rank(strongSide, strongPawn); Direction push = pawn_push(strongSide); // If the pawn is on the 5th rank and the pawn (currently) is on @@ -541,11 +523,11 @@ ScaleFactor Endgame::operator()(const Position& pos) const { // a fortress. Depending on the king position give a moderate // reduction or a stronger one if the defending king is near the // corner but not trapped there. - if (rk == RANK_5 && !opposite_colors(bsq, psq)) + if (pawnRank == RANK_5 && !opposite_colors(weakBishop, strongPawn)) { - int d = distance(psq + 3 * push, ksq); + int d = distance(strongPawn + 3 * push, weakKing); - if (d <= 2 && !(d == 0 && ksq == pos.square(strongSide) + 2 * push)) + if (d <= 2 && !(d == 0 && weakKing == strongKing + 2 * push)) return ScaleFactor(24); else return ScaleFactor(48); @@ -555,10 +537,10 @@ ScaleFactor Endgame::operator()(const Position& pos) const { // it's drawn if the bishop attacks the square in front of the // pawn from a reasonable distance and the defending king is near // the corner - if ( rk == RANK_6 - && distance(psq + 2 * push, ksq) <= 1 - && (PseudoAttacks[BISHOP][bsq] & (psq + push)) - && distance(bsq, psq) >= 2) + if ( pawnRank == RANK_6 + && distance(strongPawn + 2 * push, weakKing) <= 1 + && (attacks_bb(weakBishop) & (strongPawn + push)) + && distance(weakBishop, strongPawn) >= 2) return ScaleFactor(8); } @@ -573,28 +555,28 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, RookValueMg, 2)); assert(verify_material(pos, weakSide, RookValueMg, 1)); - Square wpsq1 = pos.squares(strongSide)[0]; - Square wpsq2 = pos.squares(strongSide)[1]; - Square bksq = pos.square(weakSide); + Square strongPawn1 = lsb(pos.pieces(strongSide, PAWN)); + Square strongPawn2 = msb(pos.pieces(strongSide, PAWN)); + Square weakKing = pos.square(weakSide); // Does the stronger side have a passed pawn? - if (pos.pawn_passed(strongSide, wpsq1) || pos.pawn_passed(strongSide, wpsq2)) + if (pos.pawn_passed(strongSide, strongPawn1) || pos.pawn_passed(strongSide, strongPawn2)) return SCALE_FACTOR_NONE; - Rank r = std::max(relative_rank(strongSide, wpsq1), relative_rank(strongSide, wpsq2)); + Rank pawnRank = std::max(relative_rank(strongSide, strongPawn1), relative_rank(strongSide, strongPawn2)); - if ( distance(bksq, wpsq1) <= 1 - && distance(bksq, wpsq2) <= 1 - && relative_rank(strongSide, bksq) > r) + if ( distance(weakKing, strongPawn1) <= 1 + && distance(weakKing, strongPawn2) <= 1 + && relative_rank(strongSide, weakKing) > pawnRank) { - assert(r > RANK_1 && r < RANK_7); - return ScaleFactor(KRPPKRPScaleFactors[r]); + assert(pawnRank > RANK_1 && pawnRank < RANK_7); + return ScaleFactor(7 * pawnRank); } return SCALE_FACTOR_NONE; } -/// K and two or more pawns vs K. There is just a single rule here: If all pawns +/// K and two or more pawns vs K. There is just a single rule here: if all pawns /// are on the same rook file and are blocked by the defending king, it's a draw. template<> ScaleFactor Endgame::operator()(const Position& pos) const { @@ -603,14 +585,12 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(pos.count(strongSide) >= 2); assert(verify_material(pos, weakSide, VALUE_ZERO, 0)); - Square ksq = pos.square(weakSide); - Bitboard pawns = pos.pieces(strongSide, PAWN); + Square weakKing = pos.square(weakSide); + Bitboard strongPawns = pos.pieces(strongSide, PAWN); - // If all pawns are ahead of the king, on a single rook file and - // the king is within one file of the pawns, it's a draw. - if ( !(pawns & ~forward_ranks_bb(weakSide, ksq)) - && !((pawns & ~FileABB) && (pawns & ~FileHBB)) - && distance(ksq, lsb(pawns)) <= 1) + // If all pawns are ahead of the king on a single rook file, it's a draw. + if ( !(strongPawns & ~(FileABB | FileHBB)) + && !(strongPawns & ~passed_pawn_span(weakSide, weakKing))) return SCALE_FACTOR_DRAW; return SCALE_FACTOR_NONE; @@ -627,20 +607,19 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, BishopValueMg, 1)); assert(verify_material(pos, weakSide, BishopValueMg, 0)); - Square pawnSq = pos.square(strongSide); - Square strongBishopSq = pos.square(strongSide); - Square weakBishopSq = pos.square(weakSide); - Square weakKingSq = pos.square(weakSide); + Square strongPawn = pos.square(strongSide); + Square strongBishop = pos.square(strongSide); + Square weakBishop = pos.square(weakSide); + Square weakKing = pos.square(weakSide); // Case 1: Defending king blocks the pawn, and cannot be driven away - if ( file_of(weakKingSq) == file_of(pawnSq) - && relative_rank(strongSide, pawnSq) < relative_rank(strongSide, weakKingSq) - && ( opposite_colors(weakKingSq, strongBishopSq) - || relative_rank(strongSide, weakKingSq) <= RANK_6)) + if ( (forward_file_bb(strongSide, strongPawn) & weakKing) + && ( opposite_colors(weakKing, strongBishop) + || relative_rank(strongSide, weakKing) <= RANK_6)) return SCALE_FACTOR_DRAW; // Case 2: Opposite colored bishops - if (opposite_colors(strongBishopSq, weakBishopSq)) + if (opposite_colors(strongBishop, weakBishop)) return SCALE_FACTOR_DRAW; return SCALE_FACTOR_NONE; @@ -654,36 +633,36 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, BishopValueMg, 2)); assert(verify_material(pos, weakSide, BishopValueMg, 0)); - Square wbsq = pos.square(strongSide); - Square bbsq = pos.square(weakSide); + Square strongBishop = pos.square(strongSide); + Square weakBishop = pos.square(weakSide); - if (!opposite_colors(wbsq, bbsq)) + if (!opposite_colors(strongBishop, weakBishop)) return SCALE_FACTOR_NONE; - Square ksq = pos.square(weakSide); - Square psq1 = pos.squares(strongSide)[0]; - Square psq2 = pos.squares(strongSide)[1]; + Square weakKing = pos.square(weakSide); + Square strongPawn1 = lsb(pos.pieces(strongSide, PAWN)); + Square strongPawn2 = msb(pos.pieces(strongSide, PAWN)); Square blockSq1, blockSq2; - if (relative_rank(strongSide, psq1) > relative_rank(strongSide, psq2)) + if (relative_rank(strongSide, strongPawn1) > relative_rank(strongSide, strongPawn2)) { - blockSq1 = psq1 + pawn_push(strongSide); - blockSq2 = make_square(file_of(psq2), rank_of(psq1)); + blockSq1 = strongPawn1 + pawn_push(strongSide); + blockSq2 = make_square(file_of(strongPawn2), rank_of(strongPawn1)); } else { - blockSq1 = psq2 + pawn_push(strongSide); - blockSq2 = make_square(file_of(psq1), rank_of(psq2)); + blockSq1 = strongPawn2 + pawn_push(strongSide); + blockSq2 = make_square(file_of(strongPawn1), rank_of(strongPawn2)); } - switch (distance(psq1, psq2)) + switch (distance(strongPawn1, strongPawn2)) { case 0: // Both pawns are on the same file. It's an easy draw if the defender firmly // controls some square in the frontmost pawn's path. - if ( file_of(ksq) == file_of(blockSq1) - && relative_rank(strongSide, ksq) >= relative_rank(strongSide, blockSq1) - && opposite_colors(ksq, wbsq)) + if ( file_of(weakKing) == file_of(blockSq1) + && relative_rank(strongSide, weakKing) >= relative_rank(strongSide, blockSq1) + && opposite_colors(weakKing, strongBishop)) return SCALE_FACTOR_DRAW; else return SCALE_FACTOR_NONE; @@ -692,17 +671,17 @@ ScaleFactor Endgame::operator()(const Position& pos) const { // Pawns on adjacent files. It's a draw if the defender firmly controls the // square in front of the frontmost pawn's path, and the square diagonally // behind this square on the file of the other pawn. - if ( ksq == blockSq1 - && opposite_colors(ksq, wbsq) - && ( bbsq == blockSq2 - || (pos.attacks_from(blockSq2) & pos.pieces(weakSide, BISHOP)) - || distance(psq1, psq2) >= 2)) + if ( weakKing == blockSq1 + && opposite_colors(weakKing, strongBishop) + && ( weakBishop == blockSq2 + || (attacks_bb(blockSq2, pos.pieces()) & pos.pieces(weakSide, BISHOP)) + || distance(strongPawn1, strongPawn2) >= 2)) return SCALE_FACTOR_DRAW; - else if ( ksq == blockSq2 - && opposite_colors(ksq, wbsq) - && ( bbsq == blockSq1 - || (pos.attacks_from(blockSq1) & pos.pieces(weakSide, BISHOP)))) + else if ( weakKing == blockSq2 + && opposite_colors(weakKing, strongBishop) + && ( weakBishop == blockSq1 + || (attacks_bb(blockSq1, pos.pieces()) & pos.pieces(weakSide, BISHOP)))) return SCALE_FACTOR_DRAW; else return SCALE_FACTOR_NONE; @@ -714,7 +693,7 @@ ScaleFactor Endgame::operator()(const Position& pos) const { } -/// KBP vs KN. There is a single rule: If the defending king is somewhere along +/// KBP vs KN. There is a single rule: if the defending king is somewhere along /// the path of the pawn, and the square of the king is not of the same color as /// the stronger side's bishop, it's a draw. template<> @@ -723,62 +702,22 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, strongSide, BishopValueMg, 1)); assert(verify_material(pos, weakSide, KnightValueMg, 0)); - Square pawnSq = pos.square(strongSide); - Square strongBishopSq = pos.square(strongSide); - Square weakKingSq = pos.square(weakSide); + Square strongPawn = pos.square(strongSide); + Square strongBishop = pos.square(strongSide); + Square weakKing = pos.square(weakSide); - if ( file_of(weakKingSq) == file_of(pawnSq) - && relative_rank(strongSide, pawnSq) < relative_rank(strongSide, weakKingSq) - && ( opposite_colors(weakKingSq, strongBishopSq) - || relative_rank(strongSide, weakKingSq) <= RANK_6)) + if ( file_of(weakKing) == file_of(strongPawn) + && relative_rank(strongSide, strongPawn) < relative_rank(strongSide, weakKing) + && ( opposite_colors(weakKing, strongBishop) + || relative_rank(strongSide, weakKing) <= RANK_6)) return SCALE_FACTOR_DRAW; return SCALE_FACTOR_NONE; } -/// KNP vs K. There is a single rule: if the pawn is a rook pawn on the 7th rank -/// and the defending king prevents the pawn from advancing, the position is drawn. -template<> -ScaleFactor Endgame::operator()(const Position& pos) const { - - assert(verify_material(pos, strongSide, KnightValueMg, 1)); - assert(verify_material(pos, weakSide, VALUE_ZERO, 0)); - - // Assume strongSide is white and the pawn is on files A-D - Square pawnSq = normalize(pos, strongSide, pos.square(strongSide)); - Square weakKingSq = normalize(pos, strongSide, pos.square(weakSide)); - - if (pawnSq == SQ_A7 && distance(SQ_A8, weakKingSq) <= 1) - return SCALE_FACTOR_DRAW; - - return SCALE_FACTOR_NONE; -} - - -/// KNP vs KB. If knight can block bishop from taking pawn, it's a win. -/// Otherwise the position is drawn. -template<> -ScaleFactor Endgame::operator()(const Position& pos) const { - - assert(verify_material(pos, strongSide, KnightValueMg, 1)); - assert(verify_material(pos, weakSide, BishopValueMg, 0)); - - Square pawnSq = pos.square(strongSide); - Square bishopSq = pos.square(weakSide); - Square weakKingSq = pos.square(weakSide); - - // King needs to get close to promoting pawn to prevent knight from blocking. - // Rules for this are very tricky, so just approximate. - if (forward_file_bb(strongSide, pawnSq) & pos.attacks_from(bishopSq)) - return ScaleFactor(distance(weakKingSq, pawnSq)); - - return SCALE_FACTOR_NONE; -} - - /// KP vs KP. This is done by removing the weakest side's pawn and probing the -/// KP vs K bitbase: If the weakest side has a draw without the pawn, it probably +/// KP vs K bitbase: if the weakest side has a draw without the pawn, it probably /// has at least a draw with the pawn as well. The exception is when the stronger /// side's pawn is far advanced and not on a rook file; in this case it is often /// possible to win (e.g. 8/4k3/3p4/3P4/6K1/8/8/8 w - - 0 1). @@ -789,18 +728,20 @@ ScaleFactor Endgame::operator()(const Position& pos) const { assert(verify_material(pos, weakSide, VALUE_ZERO, 1)); // Assume strongSide is white and the pawn is on files A-D - Square wksq = normalize(pos, strongSide, pos.square(strongSide)); - Square bksq = normalize(pos, strongSide, pos.square(weakSide)); - Square psq = normalize(pos, strongSide, pos.square(strongSide)); + Square strongKing = normalize(pos, strongSide, pos.square(strongSide)); + Square weakKing = normalize(pos, strongSide, pos.square(weakSide)); + Square strongPawn = normalize(pos, strongSide, pos.square(strongSide)); Color us = strongSide == pos.side_to_move() ? WHITE : BLACK; // If the pawn has advanced to the fifth rank or further, and is not a // rook pawn, it's too dangerous to assume that it's at least a draw. - if (rank_of(psq) >= RANK_5 && file_of(psq) != FILE_A) + if (rank_of(strongPawn) >= RANK_5 && file_of(strongPawn) != FILE_A) return SCALE_FACTOR_NONE; // Probe the KPK bitbase with the weakest side's pawn removed. If it's a draw, // it's probably at least a draw even with the pawn. - return Bitbases::probe(wksq, psq, bksq, us) ? SCALE_FACTOR_NONE : SCALE_FACTOR_DRAW; + return Bitbases::probe(strongKing, strongPawn, weakKing, us) ? SCALE_FACTOR_NONE : SCALE_FACTOR_DRAW; } + +} // namespace Stockfish diff --git a/src/endgame.h b/src/endgame.h index 4642e44857b..e79f696fa3a 100644 --- a/src/endgame.h +++ b/src/endgame.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,15 +19,16 @@ #ifndef ENDGAME_H_INCLUDED #define ENDGAME_H_INCLUDED -#include #include #include #include +#include #include #include "position.h" #include "types.h" +namespace Stockfish { /// EndgameCode lists all supported endgame functions by corresponding codes @@ -57,8 +56,6 @@ enum EndgameCode { KBPKB, // KBP vs KB KBPPKB, // KBPP vs KB KBPKN, // KBP vs KN - KNPK, // KNP vs K - KNPKB, // KNP vs KB KPKP // KP vs KP }; @@ -124,4 +121,6 @@ namespace Endgames { } } +} // namespace Stockfish + #endif // #ifndef ENDGAME_H_INCLUDED diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 7c7ce95cebc..a5c049a88ad 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,22 +18,142 @@ #include #include +#include #include // For std::memset +#include #include #include +#include +#include +#include #include "bitboard.h" #include "evaluate.h" #include "material.h" +#include "misc.h" #include "pawns.h" #include "thread.h" +#include "timeman.h" +#include "uci.h" +#include "incbin/incbin.h" + + +// Macro to embed the default efficiently updatable neural network (NNUE) file +// data in the engine binary (using incbin.h, by Dale Weiler). +// This macro invocation will declare the following three variables +// const unsigned char gEmbeddedNNUEData[]; // a pointer to the embedded data +// const unsigned char *const gEmbeddedNNUEEnd; // a marker to the end +// const unsigned int gEmbeddedNNUESize; // the size of the embedded file +// Note that this does not work in Microsoft Visual Studio. +#if !defined(_MSC_VER) && !defined(NNUE_EMBEDDING_OFF) + INCBIN(EmbeddedNNUE, EvalFileDefaultName); +#else + const unsigned char gEmbeddedNNUEData[1] = {0x0}; + const unsigned char *const gEmbeddedNNUEEnd = &gEmbeddedNNUEData[1]; + const unsigned int gEmbeddedNNUESize = 1; +#endif + + +using namespace std; + +namespace Stockfish { + +namespace Eval { + + bool useNNUE; + string currentEvalFileName = "None"; + + /// NNUE::init() tries to load a NNUE network at startup time, or when the engine + /// receives a UCI command "setoption name EvalFile value nn-[a-z0-9]{12}.nnue" + /// The name of the NNUE network is always retrieved from the EvalFile option. + /// We search the given network in three locations: internally (the default + /// network may be embedded in the binary), in the active working directory and + /// in the engine directory. Distro packagers may define the DEFAULT_NNUE_DIRECTORY + /// variable to have the engine search in a special directory in their distro. + + void NNUE::init() { + + useNNUE = Options["Use NNUE"]; + if (!useNNUE) + return; + + string eval_file = string(Options["EvalFile"]); + if (eval_file.empty()) + eval_file = EvalFileDefaultName; + + #if defined(DEFAULT_NNUE_DIRECTORY) + #define stringify2(x) #x + #define stringify(x) stringify2(x) + vector dirs = { "" , "" , CommandLine::binaryDirectory , stringify(DEFAULT_NNUE_DIRECTORY) }; + #else + vector dirs = { "" , "" , CommandLine::binaryDirectory }; + #endif + + for (string directory : dirs) + if (currentEvalFileName != eval_file) + { + if (directory != "") + { + ifstream stream(directory + eval_file, ios::binary); + if (load_eval(eval_file, stream)) + currentEvalFileName = eval_file; + } + + if (directory == "" && eval_file == EvalFileDefaultName) + { + // C++ way to prepare a buffer for a memory stream + class MemoryBuffer : public basic_streambuf { + public: MemoryBuffer(char* p, size_t n) { setg(p, p, p + n); setp(p, p + n); } + }; + + MemoryBuffer buffer(const_cast(reinterpret_cast(gEmbeddedNNUEData)), + size_t(gEmbeddedNNUESize)); + + istream stream(&buffer); + if (load_eval(eval_file, stream)) + currentEvalFileName = eval_file; + } + } + } + + /// NNUE::verify() verifies that the last net used was loaded successfully + void NNUE::verify() { + + string eval_file = string(Options["EvalFile"]); + if (eval_file.empty()) + eval_file = EvalFileDefaultName; + + if (useNNUE && currentEvalFileName != eval_file) + { + + string msg1 = "If the UCI option \"Use NNUE\" is set to true, network evaluation parameters compatible with the engine must be available."; + string msg2 = "The option is set to true, but the network file " + eval_file + " was not loaded successfully."; + string msg3 = "The UCI option EvalFile might need to specify the full path, including the directory name, to the network file."; + string msg4 = "The default net can be downloaded from: https://tests.stockfishchess.org/api/nn/" + std::string(EvalFileDefaultName); + string msg5 = "The engine will be terminated now."; + + sync_cout << "info string ERROR: " << msg1 << sync_endl; + sync_cout << "info string ERROR: " << msg2 << sync_endl; + sync_cout << "info string ERROR: " << msg3 << sync_endl; + sync_cout << "info string ERROR: " << msg4 << sync_endl; + sync_cout << "info string ERROR: " << msg5 << sync_endl; + + exit(EXIT_FAILURE); + } + + if (useNNUE) + sync_cout << "info string NNUE evaluation using " << eval_file << " enabled" << sync_endl; + else + sync_cout << "info string classical evaluation enabled" << sync_endl; + } +} namespace Trace { enum Tracing { NO_TRACE, TRACE }; enum Term { // The first 8 entries are reserved for PieceType - MATERIAL = 8, IMBALANCE, MOBILITY, THREAT, PASSED, SPACE, INITIATIVE, TOTAL, TERM_NB + MATERIAL = 8, IMBALANCE, MOBILITY, THREAT, PASSED, SPACE, WINNABLE, TOTAL, TERM_NB }; Score scores[TERM_NB][COLOR_NB]; @@ -59,12 +177,12 @@ namespace Trace { std::ostream& operator<<(std::ostream& os, Term t) { - if (t == MATERIAL || t == IMBALANCE || t == INITIATIVE || t == TOTAL) + if (t == MATERIAL || t == IMBALANCE || t == WINNABLE || t == TOTAL) os << " ---- ----" << " | " << " ---- ----"; else os << scores[t][WHITE] << " | " << scores[t][BLACK]; - os << " | " << scores[t][WHITE] - scores[t][BLACK] << "\n"; + os << " | " << scores[t][WHITE] - scores[t][BLACK] << " |\n"; return os; } } @@ -74,79 +192,95 @@ using namespace Trace; namespace { // Threshold for lazy and space evaluation - constexpr Value LazyThreshold = Value(1400); - constexpr Value SpaceThreshold = Value(12222); + constexpr Value LazyThreshold1 = Value(3631); + constexpr Value LazyThreshold2 = Value(2084); + constexpr Value SpaceThreshold = Value(11551); // KingAttackWeights[PieceType] contains king attack weights by piece type constexpr int KingAttackWeights[PIECE_TYPE_NB] = { 0, 0, 81, 52, 44, 10 }; - // Penalties for enemy's safe checks - constexpr int QueenSafeCheck = 780; - constexpr int RookSafeCheck = 1080; - constexpr int BishopSafeCheck = 635; - constexpr int KnightSafeCheck = 790; + // SafeCheck[PieceType][single/multiple] contains safe check bonus by piece type, + // higher if multiple safe checks are possible for that piece type. + constexpr int SafeCheck[][2] = { + {}, {}, {803, 1292}, {639, 974}, {1087, 1878}, {759, 1132} + }; #define S(mg, eg) make_score(mg, eg) // MobilityBonus[PieceType-2][attacked] contains bonuses for middle and end game, // indexed by piece type and number of attacked squares in the mobility area. constexpr Score MobilityBonus[][32] = { - { S(-62,-81), S(-53,-56), S(-12,-30), S( -4,-14), S( 3, 8), S( 13, 15), // Knights - S( 22, 23), S( 28, 27), S( 33, 33) }, - { S(-48,-59), S(-20,-23), S( 16, -3), S( 26, 13), S( 38, 24), S( 51, 42), // Bishops - S( 55, 54), S( 63, 57), S( 63, 65), S( 68, 73), S( 81, 78), S( 81, 86), - S( 91, 88), S( 98, 97) }, - { S(-58,-76), S(-27,-18), S(-15, 28), S(-10, 55), S( -5, 69), S( -2, 82), // Rooks - S( 9,112), S( 16,118), S( 30,132), S( 29,142), S( 32,155), S( 38,165), - S( 46,166), S( 48,169), S( 58,171) }, - { S(-39,-36), S(-21,-15), S( 3, 8), S( 3, 18), S( 14, 34), S( 22, 54), // Queens - S( 28, 61), S( 41, 73), S( 43, 79), S( 48, 92), S( 56, 94), S( 60,104), - S( 60,113), S( 66,120), S( 67,123), S( 70,126), S( 71,133), S( 73,136), - S( 79,140), S( 88,143), S( 88,148), S( 99,166), S(102,170), S(102,175), - S(106,184), S(109,191), S(113,206), S(116,212) } + { S(-62,-79), S(-53,-57), S(-12,-31), S( -3,-17), S( 3, 7), S( 12, 13), // Knight + S( 21, 16), S( 28, 21), S( 37, 26) }, + { S(-47,-59), S(-20,-25), S( 14, -8), S( 29, 12), S( 39, 21), S( 53, 40), // Bishop + S( 53, 56), S( 60, 58), S( 62, 65), S( 69, 72), S( 78, 78), S( 83, 87), + S( 91, 88), S( 96, 98) }, + { S(-60,-82), S(-24,-15), S( 0, 17) ,S( 3, 43), S( 4, 72), S( 14,100), // Rook + S( 20,102), S( 30,122), S( 41,133), S(41 ,139), S( 41,153), S( 45,160), + S( 57,165), S( 58,170), S( 67,175) }, + { S(-29,-49), S(-16,-29), S( -8, -8), S( -8, 17), S( 18, 39), S( 25, 54), // Queen + S( 23, 59), S( 37, 73), S( 41, 76), S( 54, 95), S( 65, 95) ,S( 68,101), + S( 69,124), S( 70,128), S( 70,132), S( 70,133) ,S( 71,136), S( 72,140), + S( 74,147), S( 76,149), S( 90,153), S(104,169), S(105,171), S(106,171), + S(112,178), S(114,185), S(114,187), S(119,221) } + }; + + // BishopPawns[distance from edge] contains a file-dependent penalty for pawns on + // squares of the same color as our bishop. + constexpr Score BishopPawns[int(FILE_NB) / 2] = { + S(3, 8), S(3, 9), S(2, 8), S(3, 8) }; - // RookOnFile[semiopen/open] contains bonuses for each rook when there is - // no (friendly) pawn on the rook file. - constexpr Score RookOnFile[] = { S(21, 4), S(47, 25) }; + // KingProtector[knight/bishop] contains penalty for each distance unit to own king + constexpr Score KingProtector[] = { S(8, 9), S(6, 9) }; + + // Outpost[knight/bishop] contains bonuses for each knight or bishop occupying a + // pawn protected square on rank 4 to 6 which is also safe from a pawn attack. + constexpr Score Outpost[] = { S(57, 38), S(31, 24) }; + + // PassedRank[Rank] contains a bonus according to the rank of a passed pawn + constexpr Score PassedRank[RANK_NB] = { + S(0, 0), S(7, 27), S(16, 32), S(17, 40), S(64, 71), S(170, 174), S(278, 262) + }; + + constexpr Score RookOnClosedFile = S(10, 5); + constexpr Score RookOnOpenFile[] = { S(19, 6), S(47, 26) }; // ThreatByMinor/ByRook[attacked PieceType] contains bonuses according to // which piece type attacks which one. Attacks on lesser pieces which are // pawn-defended are not considered. constexpr Score ThreatByMinor[PIECE_TYPE_NB] = { - S(0, 0), S(6, 32), S(59, 41), S(79, 56), S(90, 119), S(79, 161) + S(0, 0), S(5, 32), S(55, 41), S(77, 56), S(89, 119), S(79, 162) }; constexpr Score ThreatByRook[PIECE_TYPE_NB] = { - S(0, 0), S(3, 44), S(38, 71), S(38, 61), S(0, 38), S(51, 38) + S(0, 0), S(3, 44), S(37, 68), S(42, 60), S(0, 39), S(58, 43) }; - // PassedRank[Rank] contains a bonus according to the rank of a passed pawn - constexpr Score PassedRank[RANK_NB] = { - S(0, 0), S(10, 28), S(17, 33), S(15, 41), S(62, 72), S(168, 177), S(276, 260) - }; + constexpr Value CorneredBishop = Value(50); // Assorted bonuses and penalties - constexpr Score BishopPawns = S( 3, 7); - constexpr Score CorneredBishop = S( 50, 50); - constexpr Score FlankAttacks = S( 8, 0); - constexpr Score Hanging = S( 69, 36); - constexpr Score KingProtector = S( 7, 8); - constexpr Score KnightOnQueen = S( 16, 12); - constexpr Score LongDiagonalBishop = S( 45, 0); - constexpr Score MinorBehindPawn = S( 18, 3); - constexpr Score Outpost = S( 30, 21); - constexpr Score PassedFile = S( 11, 8); - constexpr Score PawnlessFlank = S( 17, 95); - constexpr Score RestrictedPiece = S( 7, 7); - constexpr Score ReachableOutpost = S( 32, 10); - constexpr Score RookOnQueenFile = S( 7, 6); - constexpr Score SliderOnQueen = S( 59, 18); - constexpr Score ThreatByKing = S( 24, 89); - constexpr Score ThreatByPawnPush = S( 48, 39); - constexpr Score ThreatBySafePawn = S(173, 94); - constexpr Score TrappedRook = S( 52, 10); - constexpr Score WeakQueen = S( 49, 15); + constexpr Score UncontestedOutpost = S( 1, 10); + constexpr Score BishopOnKingRing = S( 24, 0); + constexpr Score BishopXRayPawns = S( 4, 5); + constexpr Score FlankAttacks = S( 8, 0); + constexpr Score Hanging = S( 69, 36); + constexpr Score KnightOnQueen = S( 16, 11); + constexpr Score LongDiagonalBishop = S( 45, 0); + constexpr Score MinorBehindPawn = S( 18, 3); + constexpr Score PassedFile = S( 11, 8); + constexpr Score PawnlessFlank = S( 17, 95); + constexpr Score ReachableOutpost = S( 31, 22); + constexpr Score RestrictedPiece = S( 7, 7); + constexpr Score RookOnKingRing = S( 16, 0); + constexpr Score SliderOnQueen = S( 60, 18); + constexpr Score ThreatByKing = S( 24, 89); + constexpr Score ThreatByPawnPush = S( 48, 39); + constexpr Score ThreatBySafePawn = S(173, 94); + constexpr Score TrappedRook = S( 55, 13); + constexpr Score WeakQueenProtection = S( 14, 0); + constexpr Score WeakQueen = S( 56, 15); + #undef S @@ -167,8 +301,7 @@ namespace { template Score threats() const; template Score passed() const; template Score space() const; - ScaleFactor scale_factor(Value eg) const; - Score initiative(Score score) const; + Value winnable(Score score) const; const Position& pos; Material::Entry* me; @@ -210,10 +343,11 @@ namespace { // Evaluation::initialize() computes king and pawn attacks, and the king ring // bitboard for a given color. This is done at the beginning of the evaluation. + template template void Evaluation::initialize() { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Up = pawn_push(Us); constexpr Direction Down = -Up; constexpr Bitboard LowRanks = (Us == WHITE ? Rank2BB | Rank3BB : Rank7BB | Rank6BB); @@ -230,15 +364,15 @@ namespace { mobilityArea[Us] = ~(b | pos.pieces(Us, KING, QUEEN) | pos.blockers_for_king(Us) | pe->pawn_attacks(Them)); // Initialize attackedBy[] for king and pawns - attackedBy[Us][KING] = pos.attacks_from(ksq); + attackedBy[Us][KING] = attacks_bb(ksq); attackedBy[Us][PAWN] = pe->pawn_attacks(Us); attackedBy[Us][ALL_PIECES] = attackedBy[Us][KING] | attackedBy[Us][PAWN]; attackedBy2[Us] = dblAttackByPawn | (attackedBy[Us][KING] & attackedBy[Us][PAWN]); // Init our king safety tables - Square s = make_square(clamp(file_of(ksq), FILE_B, FILE_G), - clamp(rank_of(ksq), RANK_2, RANK_7)); - kingRing[Us] = PseudoAttacks[KING][s] | s; + Square s = make_square(std::clamp(file_of(ksq), FILE_B, FILE_G), + std::clamp(rank_of(ksq), RANK_2, RANK_7)); + kingRing[Us] = attacks_bb(s) | s; kingAttackersCount[Them] = popcount(kingRing[Us] & pe->pawn_attacks(Them)); kingAttacksCount[Them] = kingAttackersWeight[Them] = 0; @@ -249,29 +383,31 @@ namespace { // Evaluation::pieces() scores pieces of a given color and type + template template Score Evaluation::pieces() { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Down = -pawn_push(Us); constexpr Bitboard OutpostRanks = (Us == WHITE ? Rank4BB | Rank5BB | Rank6BB : Rank5BB | Rank4BB | Rank3BB); - const Square* pl = pos.squares(Us); - + Bitboard b1 = pos.pieces(Us, Pt); Bitboard b, bb; Score score = SCORE_ZERO; attackedBy[Us][Pt] = 0; - for (Square s = *pl; s != SQ_NONE; s = *++pl) + while (b1) { + Square s = pop_lsb(b1); + // Find attacked squares, including x-ray attacks for bishops and rooks b = Pt == BISHOP ? attacks_bb(s, pos.pieces() ^ pos.pieces(QUEEN)) : Pt == ROOK ? attacks_bb< ROOK>(s, pos.pieces() ^ pos.pieces(QUEEN) ^ pos.pieces(Us, ROOK)) - : pos.attacks_from(s); + : attacks_bb(s, pos.pieces()); if (pos.blockers_for_king(Us) & s) - b &= LineBB[pos.square(Us)][s]; + b &= line_bb(pos.square(Us), s); attackedBy2[Us] |= attackedBy[Us][ALL_PIECES] & b; attackedBy[Us][Pt] |= b; @@ -284,35 +420,52 @@ namespace { kingAttacksCount[Us] += popcount(b & attackedBy[Them][KING]); } - int mob = popcount(b & mobilityArea[Us]); + else if (Pt == ROOK && (file_bb(s) & kingRing[Them])) + score += RookOnKingRing; + + else if (Pt == BISHOP && (attacks_bb(s, pos.pieces(PAWN)) & kingRing[Them])) + score += BishopOnKingRing; + int mob = popcount(b & mobilityArea[Us]); mobility[Us] += MobilityBonus[Pt - 2][mob]; if (Pt == BISHOP || Pt == KNIGHT) { - // Bonus if piece is on an outpost square or can reach one - bb = OutpostRanks & attackedBy[Us][PAWN] & ~pe->pawn_attacks_span(Them); - if (bb & s) - score += Outpost * (Pt == KNIGHT ? 2 : 1); - + // Bonus if the piece is on an outpost square or can reach one + // Bonus for knights (UncontestedOutpost) if few relevant targets + bb = OutpostRanks & (attackedBy[Us][PAWN] | shift(pos.pieces(PAWN))) + & ~pe->pawn_attacks_span(Them); + Bitboard targets = pos.pieces(Them) & ~pos.pieces(PAWN); + + if ( Pt == KNIGHT + && bb & s & ~CenterFiles // on a side outpost + && !(b & targets) // no relevant attacks + && (!more_than_one(targets & (s & QueenSide ? QueenSide : KingSide)))) + score += UncontestedOutpost * popcount(pos.pieces(PAWN) & (s & QueenSide ? QueenSide : KingSide)); + else if (bb & s) + score += Outpost[Pt == BISHOP]; else if (Pt == KNIGHT && bb & b & ~pos.pieces(Us)) score += ReachableOutpost; - // Knight and Bishop bonus for being right behind a pawn + // Bonus for a knight or bishop shielded by pawn if (shift(pos.pieces(PAWN)) & s) score += MinorBehindPawn; // Penalty if the piece is far from the king - score -= KingProtector * distance(s, pos.square(Us)); + score -= KingProtector[Pt == BISHOP] * distance(pos.square(Us), s); - if (Pt == BISHOP) + if constexpr (Pt == BISHOP) { - // Penalty according to number of pawns on the same color square as the - // bishop, bigger when the center files are blocked with pawns. + // Penalty according to the number of our pawns on the same color square as the + // bishop, bigger when the center files are blocked with pawns and smaller + // when the bishop is outside the pawn chain. Bitboard blocked = pos.pieces(Us, PAWN) & shift(pos.pieces()); - score -= BishopPawns * pos.pawns_on_same_color_squares(Us, s) - * (1 + popcount(blocked & CenterFiles)); + score -= BishopPawns[edge_distance(file_of(s))] * pos.pawns_on_same_color_squares(Us, s) + * (!(attackedBy[Us][PAWN] & s) + popcount(blocked & CenterFiles)); + + // Penalty for all enemy pawns x-rayed + score -= BishopXRayPawns * popcount(attacks_bb(s) & pos.pieces(Them, PAWN)); // Bonus for bishop on a long diagonal which can "see" both center squares if (more_than_one(attacks_bb(s, pos.pieces(PAWN)) & Center)) @@ -326,33 +479,40 @@ namespace { { Direction d = pawn_push(Us) + (file_of(s) == FILE_A ? EAST : WEST); if (pos.piece_on(s + d) == make_piece(Us, PAWN)) - score -= !pos.empty(s + d + pawn_push(Us)) ? CorneredBishop * 4 - : pos.piece_on(s + d + d) == make_piece(Us, PAWN) ? CorneredBishop * 2 - : CorneredBishop; + score -= !pos.empty(s + d + pawn_push(Us)) ? 4 * make_score(CorneredBishop, CorneredBishop) + : 3 * make_score(CorneredBishop, CorneredBishop); } } } - if (Pt == ROOK) + if constexpr (Pt == ROOK) { - // Bonus for rook on the same file as a queen - if (file_bb(s) & pos.pieces(QUEEN)) - score += RookOnQueenFile; - - // Bonus for rook on an open or semi-open file + // Bonuses for rook on a (semi-)open or closed file if (pos.is_on_semiopen_file(Us, s)) - score += RookOnFile[pos.is_on_semiopen_file(Them, s)]; - - // Penalty when trapped by the king, even more if the king cannot castle - else if (mob <= 3) { - File kf = file_of(pos.square(Us)); - if ((kf < FILE_E) == (file_of(s) < kf)) - score -= TrappedRook * (1 + !pos.castling_rights(Us)); + score += RookOnOpenFile[pos.is_on_semiopen_file(Them, s)]; + } + else + { + // If our pawn on this file is blocked, increase penalty + if ( pos.pieces(Us, PAWN) + & shift(pos.pieces()) + & file_bb(s)) + { + score -= RookOnClosedFile; + } + + // Penalty when trapped by the king, even more if the king cannot castle + if (mob <= 3) + { + File kf = file_of(pos.square(Us)); + if ((kf < FILE_E) == (file_of(s) < kf)) + score -= TrappedRook * (1 + !pos.castling_rights(Us)); + } } } - if (Pt == QUEEN) + if constexpr (Pt == QUEEN) { // Penalty if any relative pin or discovered attack against the queen Bitboard queenPinners; @@ -360,7 +520,7 @@ namespace { score -= WeakQueen; } } - if (T) + if constexpr (T) Trace::add(Pt, Us, score); return score; @@ -368,10 +528,11 @@ namespace { // Evaluation::king() assigns bonuses and penalties to a king of a given color + template template Score Evaluation::king() const { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Bitboard Camp = (Us == WHITE ? AllSquares ^ Rank6BB ^ Rank7BB ^ Rank8BB : AllSquares ^ Rank1BB ^ Rank2BB ^ Rank3BB); @@ -396,41 +557,33 @@ namespace { b2 = attacks_bb(ksq, pos.pieces() ^ pos.pieces(Us, QUEEN)); // Enemy rooks checks - rookChecks = b1 & safe & attackedBy[Them][ROOK]; - + rookChecks = b1 & attackedBy[Them][ROOK] & safe; if (rookChecks) - kingDanger += RookSafeCheck; + kingDanger += SafeCheck[ROOK][more_than_one(rookChecks)]; else unsafeChecks |= b1 & attackedBy[Them][ROOK]; - // Enemy queen safe checks: we count them only if they are from squares from - // which we can't give a rook check, because rook checks are more valuable. - queenChecks = (b1 | b2) - & attackedBy[Them][QUEEN] - & safe - & ~attackedBy[Us][QUEEN] - & ~rookChecks; - + // Enemy queen safe checks: count them only if the checks are from squares from + // which opponent cannot give a rook check, because rook checks are more valuable. + queenChecks = (b1 | b2) & attackedBy[Them][QUEEN] & safe + & ~(attackedBy[Us][QUEEN] | rookChecks); if (queenChecks) - kingDanger += QueenSafeCheck; + kingDanger += SafeCheck[QUEEN][more_than_one(queenChecks)]; - // Enemy bishops checks: we count them only if they are from squares from - // which we can't give a queen check, because queen checks are more valuable. - bishopChecks = b2 - & attackedBy[Them][BISHOP] - & safe + // Enemy bishops checks: count them only if they are from squares from which + // opponent cannot give a queen check, because queen checks are more valuable. + bishopChecks = b2 & attackedBy[Them][BISHOP] & safe & ~queenChecks; - if (bishopChecks) - kingDanger += BishopSafeCheck; + kingDanger += SafeCheck[BISHOP][more_than_one(bishopChecks)]; + else unsafeChecks |= b2 & attackedBy[Them][BISHOP]; // Enemy knights checks - knightChecks = pos.attacks_from(ksq) & attackedBy[Them][KNIGHT]; - + knightChecks = attacks_bb(ksq) & attackedBy[Them][KNIGHT]; if (knightChecks & safe) - kingDanger += KnightSafeCheck; + kingDanger += SafeCheck[KNIGHT][more_than_one(knightChecks & safe)]; else unsafeChecks |= knightChecks; @@ -440,21 +593,21 @@ namespace { b2 = b1 & attackedBy2[Them]; b3 = attackedBy[Us][ALL_PIECES] & KingFlank[file_of(ksq)] & Camp; - int kingFlankAttack = popcount(b1) + popcount(b2); + int kingFlankAttack = popcount(b1) + popcount(b2); int kingFlankDefense = popcount(b3); - kingDanger += kingAttackersCount[Them] * kingAttackersWeight[Them] - + 185 * popcount(kingRing[Us] & weak) - + 148 * popcount(unsafeChecks) - + 98 * popcount(pos.blockers_for_king(Us)) - + 69 * kingAttacksCount[Them] - + 3 * kingFlankAttack * kingFlankAttack / 8 - + mg_value(mobility[Them] - mobility[Us]) - - 873 * !pos.count(Them) - - 100 * bool(attackedBy[Us][KNIGHT] & attackedBy[Us][KING]) - - 6 * mg_value(score) / 8 - - 4 * kingFlankDefense - + 37; + kingDanger += kingAttackersCount[Them] * kingAttackersWeight[Them] // (~10 Elo) + + 183 * popcount(kingRing[Us] & weak) // (~15 Elo) + + 148 * popcount(unsafeChecks) // (~4 Elo) + + 98 * popcount(pos.blockers_for_king(Us)) // (~2 Elo) + + 69 * kingAttacksCount[Them] // (~0.5 Elo) + + 3 * kingFlankAttack * kingFlankAttack / 8 // (~0.5 Elo) + + mg_value(mobility[Them] - mobility[Us]) // (~0.5 Elo) + - 873 * !pos.count(Them) // (~24 Elo) + - 100 * bool(attackedBy[Us][KNIGHT] & attackedBy[Us][KING]) // (~5 Elo) + - 6 * mg_value(score) / 8 // (~8 Elo) + - 4 * kingFlankDefense // (~5 Elo) + + 37; // (~0.5 Elo) // Transform the kingDanger units into a Score, and subtract it from the evaluation if (kingDanger > 100) @@ -467,7 +620,7 @@ namespace { // Penalty if king flank is under attack, potentially moving toward the king score -= FlankAttacks * kingFlankAttack; - if (T) + if constexpr (T) Trace::add(KING, Us, score); return score; @@ -476,10 +629,11 @@ namespace { // Evaluation::threats() assigns bonuses according to the types of the // attacking and the attacked pieces. + template template Score Evaluation::threats() const { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Up = pawn_push(Us); constexpr Bitboard TRank3BB = (Us == WHITE ? Rank3BB : Rank6BB); @@ -505,11 +659,11 @@ namespace { { b = (defended | weak) & (attackedBy[Us][KNIGHT] | attackedBy[Us][BISHOP]); while (b) - score += ThreatByMinor[type_of(pos.piece_on(pop_lsb(&b)))]; + score += ThreatByMinor[type_of(pos.piece_on(pop_lsb(b)))]; b = weak & attackedBy[Us][ROOK]; while (b) - score += ThreatByRook[type_of(pos.piece_on(pop_lsb(&b)))]; + score += ThreatByRook[type_of(pos.piece_on(pop_lsb(b)))]; if (weak & attackedBy[Us][KING]) score += ThreatByKing; @@ -517,13 +671,15 @@ namespace { b = ~attackedBy[Them][ALL_PIECES] | (nonPawnEnemies & attackedBy2[Us]); score += Hanging * popcount(weak & b); + + // Additional bonus if weak piece is only protected by a queen + score += WeakQueenProtection * popcount(weak & attackedBy[Them][QUEEN]); } // Bonus for restricting their piece moves b = attackedBy[Them][ALL_PIECES] & ~stronglyProtected & attackedBy[Us][ALL_PIECES]; - score += RestrictedPiece * popcount(b); // Protected or unattacked squares @@ -548,20 +704,24 @@ namespace { // Bonus for threats on the next moves against enemy queen if (pos.count(Them) == 1) { + bool queenImbalance = pos.count() == 1; + Square s = pos.square(Them); - safe = mobilityArea[Us] & ~stronglyProtected; + safe = mobilityArea[Us] + & ~pos.pieces(Us, PAWN) + & ~stronglyProtected; - b = attackedBy[Us][KNIGHT] & pos.attacks_from(s); + b = attackedBy[Us][KNIGHT] & attacks_bb(s); - score += KnightOnQueen * popcount(b & safe); + score += KnightOnQueen * popcount(b & safe) * (1 + queenImbalance); - b = (attackedBy[Us][BISHOP] & pos.attacks_from(s)) - | (attackedBy[Us][ROOK ] & pos.attacks_from(s)); + b = (attackedBy[Us][BISHOP] & attacks_bb(s, pos.pieces())) + | (attackedBy[Us][ROOK ] & attacks_bb(s, pos.pieces())); - score += SliderOnQueen * popcount(b & safe & attackedBy2[Us]); + score += SliderOnQueen * popcount(b & safe & attackedBy2[Us]) * (1 + queenImbalance); } - if (T) + if constexpr (T) Trace::add(THREAT, Us, score); return score; @@ -573,21 +733,35 @@ namespace { template template Score Evaluation::passed() const { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Up = pawn_push(Us); + constexpr Direction Down = -Up; auto king_proximity = [&](Color c, Square s) { return std::min(distance(pos.square(c), s), 5); }; - Bitboard b, bb, squaresToQueen, unsafeSquares; + Bitboard b, bb, squaresToQueen, unsafeSquares, blockedPassers, helpers; Score score = SCORE_ZERO; b = pe->passed_pawns(Us); + blockedPassers = b & shift(pos.pieces(Them, PAWN)); + if (blockedPassers) + { + helpers = shift(pos.pieces(Us, PAWN)) + & ~pos.pieces(Them) + & (~attackedBy2[Them] | attackedBy[Us][ALL_PIECES]); + + // Remove blocked candidate passers that don't have help to pass + b &= ~blockedPassers + | shift(helpers) + | shift(helpers); + } + while (b) { - Square s = pop_lsb(&b); + Square s = pop_lsb(b); assert(!(pos.pieces(Them, PAWN) & forward_file_bb(Us, s + Up))); @@ -601,8 +775,8 @@ namespace { Square blockSq = s + Up; // Adjust bonus based on the king's proximity - bonus += make_score(0, ( (king_proximity(Them, blockSq) * 19) / 4 - - king_proximity(Us, blockSq) * 2) * w); + bonus += make_score(0, ( king_proximity(Them, blockSq) * 19 / 4 + - king_proximity(Us, blockSq) * 2) * w); // If blockSq is not the queening square then consider also a second push if (r != RANK_7) @@ -617,14 +791,16 @@ namespace { bb = forward_file_bb(Them, s) & pos.pieces(ROOK, QUEEN); if (!(pos.pieces(Them) & bb)) - unsafeSquares &= attackedBy[Them][ALL_PIECES]; + unsafeSquares &= attackedBy[Them][ALL_PIECES] | pos.pieces(Them); - // If there are no enemy attacks on passed pawn span, assign a big bonus. + // If there are no enemy pieces or attacks on passed pawn span, assign a big bonus. + // Or if there is some, but they are all attacked by our pawns, assign a bit smaller bonus. // Otherwise assign a smaller bonus if the path to queen is not attacked // and even smaller bonus if it is attacked but block square is not. - int k = !unsafeSquares ? 35 : - !(unsafeSquares & squaresToQueen) ? 20 : - !(unsafeSquares & blockSq) ? 9 : + int k = !unsafeSquares ? 36 : + !(unsafeSquares & ~attackedBy[Us][PAWN]) ? 30 : + !(unsafeSquares & squaresToQueen) ? 17 : + !(unsafeSquares & blockSq) ? 7 : 0 ; // Assign a larger bonus if the block square is defended @@ -635,36 +811,29 @@ namespace { } } // r > RANK_3 - // Scale down bonus for candidate passers which need more than one - // pawn push to become passed, or have a pawn in front of them. - if ( !pos.pawn_passed(Us, s + Up) - || (pos.pieces(PAWN) & (s + Up))) - bonus = bonus / 2; - - score += bonus - PassedFile * map_to_queenside(file_of(s)); + score += bonus - PassedFile * edge_distance(file_of(s)); } - if (T) + if constexpr (T) Trace::add(PASSED, Us, score); return score; } - // Evaluation::space() computes the space evaluation for a given side. The - // space evaluation is a simple bonus based on the number of safe squares - // available for minor pieces on the central four files on ranks 2--4. Safe - // squares one, two or three squares behind a friendly pawn are counted - // twice. Finally, the space bonus is multiplied by a weight. The aim is to - // improve play on game opening. + // Evaluation::space() computes a space evaluation for a given side, aiming to improve game + // play in the opening. It is based on the number of safe squares on the four central files + // on ranks 2 to 4. Completely safe squares behind a friendly pawn are counted twice. + // Finally, the space bonus is multiplied by a weight which decreases according to occupancy. template template Score Evaluation::space() const { + // Early exit if, for example, both queens or 6 minor pieces have been exchanged if (pos.non_pawn_material() < SpaceThreshold) return SCORE_ZERO; - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Down = -pawn_push(Us); constexpr Bitboard SpaceMask = Us == WHITE ? CenterFiles & (Rank2BB | Rank3BB | Rank4BB) @@ -680,84 +849,114 @@ namespace { behind |= shift(behind); behind |= shift(behind); + // Compute space score based on the number of safe squares and number of our pieces + // increased with number of total blocked pawns in position. int bonus = popcount(safe) + popcount(behind & safe & ~attackedBy[Them][ALL_PIECES]); - int weight = pos.count(Us) - 1; + int weight = pos.count(Us) - 3 + std::min(pe->blocked_count(), 9); Score score = make_score(bonus * weight * weight / 16, 0); - if (T) + if constexpr (T) Trace::add(SPACE, Us, score); return score; } - // Evaluation::initiative() computes the initiative correction value - // for the position. It is a second order bonus/malus based on the - // known attacking/defending status of the players. + // Evaluation::winnable() adjusts the midgame and endgame score components, based on + // the known attacking/defending status of the players. The final value is derived + // by interpolation from the midgame and endgame values. template - Score Evaluation::initiative(Score score) const { - - Value mg = mg_value(score); - Value eg = eg_value(score); + Value Evaluation::winnable(Score score) const { int outflanking = distance(pos.square(WHITE), pos.square(BLACK)) - - distance(pos.square(WHITE), pos.square(BLACK)); - - bool infiltration = rank_of(pos.square(WHITE)) > RANK_4 - || rank_of(pos.square(BLACK)) < RANK_5; + + int(rank_of(pos.square(WHITE)) - rank_of(pos.square(BLACK))); bool pawnsOnBothFlanks = (pos.pieces(PAWN) & QueenSide) && (pos.pieces(PAWN) & KingSide); - bool almostUnwinnable = !pe->passed_count() - && outflanking < 0 + bool almostUnwinnable = outflanking < 0 && !pawnsOnBothFlanks; + bool infiltration = rank_of(pos.square(WHITE)) > RANK_4 + || rank_of(pos.square(BLACK)) < RANK_5; + // Compute the initiative bonus for the attacking side int complexity = 9 * pe->passed_count() - + 11 * pos.count() + + 12 * pos.count() + 9 * outflanking - + 12 * infiltration + 21 * pawnsOnBothFlanks + + 24 * infiltration + 51 * !pos.non_pawn_material() - 43 * almostUnwinnable - - 100 ; + -110 ; + + Value mg = mg_value(score); + Value eg = eg_value(score); // Now apply the bonus: note that we find the attacking side by extracting the // sign of the midgame or endgame values, and that we carefully cap the bonus // so that the midgame and endgame scores do not change sign after the bonus. - int u = ((mg > 0) - (mg < 0)) * std::max(std::min(complexity + 50, 0), -abs(mg)); + int u = ((mg > 0) - (mg < 0)) * std::clamp(complexity + 50, -abs(mg), 0); int v = ((eg > 0) - (eg < 0)) * std::max(complexity, -abs(eg)); - if (T) - Trace::add(INITIATIVE, make_score(u, v)); - - return make_score(u, v); - } - - - // Evaluation::scale_factor() computes the scale factor for the winning side - - template - ScaleFactor Evaluation::scale_factor(Value eg) const { + mg += u; + eg += v; + // Compute the scale factor for the winning side Color strongSide = eg > VALUE_DRAW ? WHITE : BLACK; int sf = me->scale_factor(pos, strongSide); - // If scale is not already specific, scale down the endgame via general heuristics + // If scale factor is not already specific, scale up/down via general heuristics if (sf == SCALE_FACTOR_NORMAL) { - if ( pos.opposite_bishops() - && pos.non_pawn_material() == 2 * BishopValueMg) - sf = 22 ; + if (pos.opposite_bishops()) + { + // For pure opposite colored bishops endgames use scale factor + // based on the number of passed pawns of the strong side. + if ( pos.non_pawn_material(WHITE) == BishopValueMg + && pos.non_pawn_material(BLACK) == BishopValueMg) + sf = 18 + 4 * popcount(pe->passed_pawns(strongSide)); + // For every other opposite colored bishops endgames use scale factor + // based on the number of all pieces of the strong side. + else + sf = 22 + 3 * pos.count(strongSide); + } + // For rook endgames with strong side not having overwhelming pawn number advantage + // and its pawns being on one flank and weak side protecting its pieces with a king + // use lower scale factor. + else if ( pos.non_pawn_material(WHITE) == RookValueMg + && pos.non_pawn_material(BLACK) == RookValueMg + && pos.count(strongSide) - pos.count(~strongSide) <= 1 + && bool(KingSide & pos.pieces(strongSide, PAWN)) != bool(QueenSide & pos.pieces(strongSide, PAWN)) + && (attacks_bb(pos.square(~strongSide)) & pos.pieces(~strongSide, PAWN))) + sf = 36; + // For queen vs no queen endgames use scale factor + // based on number of minors of side that doesn't have queen. + else if (pos.count() == 1) + sf = 37 + 3 * (pos.count(WHITE) == 1 ? pos.count(BLACK) + pos.count(BLACK) + : pos.count(WHITE) + pos.count(WHITE)); + // In every other case use scale factor based on + // the number of pawns of the strong side reduced if pawns are on a single flank. else - sf = std::min(sf, 36 + (pos.opposite_bishops() ? 2 : 7) * pos.count(strongSide)); + sf = std::min(sf, 36 + 7 * pos.count(strongSide)) - 4 * !pawnsOnBothFlanks; + + // Reduce scale factor in case of pawns being on a single flank + sf -= 4 * !pawnsOnBothFlanks; + } - sf = std::max(0, sf - (pos.rule50_count() - 12) / 4); + // Interpolate between the middlegame and (scaled by 'sf') endgame score + v = mg * int(me->game_phase()) + + eg * int(PHASE_MIDGAME - me->game_phase()) * ScaleFactor(sf) / SCALE_FACTOR_NORMAL; + v /= PHASE_MIDGAME; + + if constexpr (T) + { + Trace::add(WINNABLE, make_score(u, eg * ScaleFactor(sf) / SCALE_FACTOR_NORMAL - eg_value(score))); + Trace::add(TOTAL, make_score(mg, eg * ScaleFactor(sf) / SCALE_FACTOR_NORMAL)); } - return ScaleFactor(sf); + return Value(v); } @@ -781,23 +980,28 @@ namespace { // Initialize score by reading the incrementally updated scores included in // the position object (material + piece square tables) and the material // imbalance. Score is computed internally from the white point of view. - Score score = pos.psq_score() + me->imbalance() + pos.this_thread()->contempt; + Score score = pos.psq_score() + me->imbalance() + pos.this_thread()->trend; // Probe the pawn hash table pe = Pawns::probe(pos); score += pe->pawn_score(WHITE) - pe->pawn_score(BLACK); // Early exit if score is high - Value v = (mg_value(score) + eg_value(score)) / 2; - if (abs(v) > LazyThreshold + pos.non_pawn_material() / 64) - return pos.side_to_move() == WHITE ? v : -v; + auto lazy_skip = [&](Value lazyThreshold) { + return abs(mg_value(score) + eg_value(score)) > lazyThreshold + + std::abs(pos.this_thread()->bestValue) * 5 / 4 + + pos.non_pawn_material() / 32; + }; - // Main evaluation begins here + if (lazy_skip(LazyThreshold1)) + goto make_v; + // Main evaluation begins here initialize(); initialize(); - // Pieces should be evaluated first (populate attack tables) + // Pieces evaluated first (also populates attackedBy, attackedBy2). + // Note that the order of evaluation of the terms is left unspecified. score += pieces() - pieces() + pieces() - pieces() + pieces() - pieces() @@ -805,84 +1009,186 @@ namespace { score += mobility[WHITE] - mobility[BLACK]; + // More complex interactions that require fully populated attack bitboards score += king< WHITE>() - king< BLACK>() - + threats() - threats() - + passed< WHITE>() - passed< BLACK>() - + space< WHITE>() - space< BLACK>(); + + passed< WHITE>() - passed< BLACK>(); - score += initiative(score); + if (lazy_skip(LazyThreshold2)) + goto make_v; - // Interpolate between a middlegame and a (scaled by 'sf') endgame score - ScaleFactor sf = scale_factor(eg_value(score)); - v = mg_value(score) * int(me->game_phase()) - + eg_value(score) * int(PHASE_MIDGAME - me->game_phase()) * sf / SCALE_FACTOR_NORMAL; + score += threats() - threats() + + space< WHITE>() - space< BLACK>(); - v /= PHASE_MIDGAME; +make_v: + // Derive single value from mg and eg parts of score + Value v = winnable(score); // In case of tracing add all remaining individual evaluation terms - if (T) + if constexpr (T) { Trace::add(MATERIAL, pos.psq_score()); Trace::add(IMBALANCE, me->imbalance()); Trace::add(PAWN, pe->pawn_score(WHITE), pe->pawn_score(BLACK)); Trace::add(MOBILITY, mobility[WHITE], mobility[BLACK]); - Trace::add(TOTAL, score); } - return (pos.side_to_move() == WHITE ? v : -v) // Side to move point of view - + Eval::Tempo; + // Evaluation grain + v = (v / 16) * 16; + + // Side to move point of view + v = (pos.side_to_move() == WHITE ? v : -v); + + return v; + } + + + /// Fisher Random Chess: correction for cornered bishops, to fix chess960 play with NNUE + + Value fix_FRC(const Position& pos) { + + constexpr Bitboard Corners = 1ULL << SQ_A1 | 1ULL << SQ_H1 | 1ULL << SQ_A8 | 1ULL << SQ_H8; + + if (!(pos.pieces(BISHOP) & Corners)) + return VALUE_ZERO; + + int correction = 0; + + if ( pos.piece_on(SQ_A1) == W_BISHOP + && pos.piece_on(SQ_B2) == W_PAWN) + correction -= CorneredBishop; + + if ( pos.piece_on(SQ_H1) == W_BISHOP + && pos.piece_on(SQ_G2) == W_PAWN) + correction -= CorneredBishop; + + if ( pos.piece_on(SQ_A8) == B_BISHOP + && pos.piece_on(SQ_B7) == B_PAWN) + correction += CorneredBishop; + + if ( pos.piece_on(SQ_H8) == B_BISHOP + && pos.piece_on(SQ_G7) == B_PAWN) + correction += CorneredBishop; + + return pos.side_to_move() == WHITE ? Value(3 * correction) + : -Value(3 * correction); } -} // namespace +} // namespace Eval /// evaluate() is the evaluator for the outer world. It returns a static /// evaluation of the position from the point of view of the side to move. Value Eval::evaluate(const Position& pos) { - return Evaluation(pos).value(); -} + Value v; + bool useClassical = false; + + // Deciding between classical and NNUE eval (~10 Elo): for high PSQ imbalance we use classical, + // but we switch to NNUE during long shuffling or with high material on the board. + if ( !useNNUE + || abs(eg_value(pos.psq_score())) * 5 > (849 + pos.non_pawn_material() / 64) * (5 + pos.rule50_count())) + { + v = Evaluation(pos).value(); // classical + useClassical = abs(v) >= 298; + } + + // If result of a classical evaluation is much lower than threshold fall back to NNUE + if (useNNUE && !useClassical) + { + Value nnue = NNUE::evaluate(pos, true); // NNUE + int scale = 1136 + 20 * pos.non_pawn_material() / 1024; + Color stm = pos.side_to_move(); + Value optimism = pos.this_thread()->optimism[stm]; + Value psq = (stm == WHITE ? 1 : -1) * eg_value(pos.psq_score()); + int complexity = 35 * abs(nnue - psq) / 256; + + optimism = optimism * (44 + complexity) / 32; + v = (nnue + optimism) * scale / 1024 - optimism; + + if (pos.is_chess960()) + v += fix_FRC(pos); + } + + // Damp down the evaluation linearly when shuffling + v = v * (208 - pos.rule50_count()) / 208; + + // Guarantee evaluation does not hit the tablebase range + v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); + + return v; +} /// trace() is like evaluate(), but instead of returning a value, it returns /// a string (suitable for outputting to stdout) that contains the detailed /// descriptions and values of each evaluation term. Useful for debugging. +/// Trace scores are from white's point of view -std::string Eval::trace(const Position& pos) { +std::string Eval::trace(Position& pos) { if (pos.checkers()) - return "Total evaluation: none (in check)"; + return "Final evaluation: none (in check)"; - std::memset(scores, 0, sizeof(scores)); + std::stringstream ss; + ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2); - pos.this_thread()->contempt = SCORE_ZERO; // Reset any dynamic contempt + Value v; - Value v = Evaluation(pos).value(); + std::memset(scores, 0, sizeof(scores)); - v = pos.side_to_move() == WHITE ? v : -v; // Trace scores are from white's point of view + // Reset any global variable used in eval + pos.this_thread()->trend = SCORE_ZERO; + pos.this_thread()->bestValue = VALUE_ZERO; + pos.this_thread()->optimism[WHITE] = VALUE_ZERO; + pos.this_thread()->optimism[BLACK] = VALUE_ZERO; + + v = Evaluation(pos).value(); - std::stringstream ss; ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2) - << " Term | White | Black | Total \n" - << " | MG EG | MG EG | MG EG \n" - << " ------------+-------------+-------------+------------\n" - << " Material | " << Term(MATERIAL) - << " Imbalance | " << Term(IMBALANCE) - << " Pawns | " << Term(PAWN) - << " Knights | " << Term(KNIGHT) - << " Bishops | " << Term(BISHOP) - << " Rooks | " << Term(ROOK) - << " Queens | " << Term(QUEEN) - << " Mobility | " << Term(MOBILITY) - << " King safety | " << Term(KING) - << " Threats | " << Term(THREAT) - << " Passed | " << Term(PASSED) - << " Space | " << Term(SPACE) - << " Initiative | " << Term(INITIATIVE) - << " ------------+-------------+-------------+------------\n" - << " Total | " << Term(TOTAL); - - ss << "\nTotal evaluation: " << to_cp(v) << " (white side)\n"; + << " Contributing terms for the classical eval:\n" + << "+------------+-------------+-------------+-------------+\n" + << "| Term | White | Black | Total |\n" + << "| | MG EG | MG EG | MG EG |\n" + << "+------------+-------------+-------------+-------------+\n" + << "| Material | " << Term(MATERIAL) + << "| Imbalance | " << Term(IMBALANCE) + << "| Pawns | " << Term(PAWN) + << "| Knights | " << Term(KNIGHT) + << "| Bishops | " << Term(BISHOP) + << "| Rooks | " << Term(ROOK) + << "| Queens | " << Term(QUEEN) + << "| Mobility | " << Term(MOBILITY) + << "|King safety | " << Term(KING) + << "| Threats | " << Term(THREAT) + << "| Passed | " << Term(PASSED) + << "| Space | " << Term(SPACE) + << "| Winnable | " << Term(WINNABLE) + << "+------------+-------------+-------------+-------------+\n" + << "| Total | " << Term(TOTAL) + << "+------------+-------------+-------------+-------------+\n"; + + if (Eval::useNNUE) + ss << '\n' << NNUE::trace(pos) << '\n'; + + ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15); + + v = pos.side_to_move() == WHITE ? v : -v; + ss << "\nClassical evaluation " << to_cp(v) << " (white side)\n"; + if (Eval::useNNUE) + { + v = NNUE::evaluate(pos, false); + v = pos.side_to_move() == WHITE ? v : -v; + ss << "NNUE evaluation " << to_cp(v) << " (white side)\n"; + } + + v = evaluate(pos); + v = pos.side_to_move() == WHITE ? v : -v; + ss << "Final evaluation " << to_cp(v) << " (white side)"; + if (Eval::useNNUE) + ss << " [with scaled NNUE, hybrid, ...]"; + ss << "\n"; return ss.str(); } + +} // namespace Stockfish diff --git a/src/evaluate.h b/src/evaluate.h index 077de70c974..57a7687d776 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,18 +20,43 @@ #define EVALUATE_H_INCLUDED #include +#include #include "types.h" +namespace Stockfish { + class Position; namespace Eval { -constexpr Value Tempo = Value(28); // Must be visible to search + std::string trace(Position& pos); + Value evaluate(const Position& pos); + + extern bool useNNUE; + extern std::string currentEvalFileName; + + // The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue + // for the build process (profile-build and fishtest) to work. Do not change the + // name of the macro, as it is used in the Makefile. + #define EvalFileDefaultName "nn-ac07bd334b62.nnue" + + namespace NNUE { + + std::string trace(Position& pos); + Value evaluate(const Position& pos, bool adjusted = false); + + void init(); + void verify(); + + bool load_eval(std::string name, std::istream& stream); + bool save_eval(std::ostream& stream); + bool save_eval(const std::optional& filename); + + } // namespace NNUE -std::string trace(const Position& pos); +} // namespace Eval -Value evaluate(const Position& pos); -} +} // namespace Stockfish #endif // #ifndef EVALUATE_H_INCLUDED diff --git a/src/incbin/UNLICENCE b/src/incbin/UNLICENCE new file mode 100644 index 00000000000..32484ab5e70 --- /dev/null +++ b/src/incbin/UNLICENCE @@ -0,0 +1,26 @@ +The file "incbin.h" is free and unencumbered software released into +the public domain by Dale Weiler, see: + + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/src/incbin/incbin.h b/src/incbin/incbin.h new file mode 100644 index 00000000000..c19684d7242 --- /dev/null +++ b/src/incbin/incbin.h @@ -0,0 +1,368 @@ +/** + * @file incbin.h + * @author Dale Weiler + * @brief Utility for including binary files + * + * Facilities for including binary files into the current translation unit and + * making use from them externally in other translation units. + */ +#ifndef INCBIN_HDR +#define INCBIN_HDR +#include +#if defined(__AVX512BW__) || \ + defined(__AVX512CD__) || \ + defined(__AVX512DQ__) || \ + defined(__AVX512ER__) || \ + defined(__AVX512PF__) || \ + defined(__AVX512VL__) || \ + defined(__AVX512F__) +# define INCBIN_ALIGNMENT_INDEX 6 +#elif defined(__AVX__) || \ + defined(__AVX2__) +# define INCBIN_ALIGNMENT_INDEX 5 +#elif defined(__SSE__) || \ + defined(__SSE2__) || \ + defined(__SSE3__) || \ + defined(__SSSE3__) || \ + defined(__SSE4_1__) || \ + defined(__SSE4_2__) || \ + defined(__neon__) +# define INCBIN_ALIGNMENT_INDEX 4 +#elif ULONG_MAX != 0xffffffffu +# define INCBIN_ALIGNMENT_INDEX 3 +# else +# define INCBIN_ALIGNMENT_INDEX 2 +#endif + +/* Lookup table of (1 << n) where `n' is `INCBIN_ALIGNMENT_INDEX' */ +#define INCBIN_ALIGN_SHIFT_0 1 +#define INCBIN_ALIGN_SHIFT_1 2 +#define INCBIN_ALIGN_SHIFT_2 4 +#define INCBIN_ALIGN_SHIFT_3 8 +#define INCBIN_ALIGN_SHIFT_4 16 +#define INCBIN_ALIGN_SHIFT_5 32 +#define INCBIN_ALIGN_SHIFT_6 64 + +/* Actual alignment value */ +#define INCBIN_ALIGNMENT \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_ALIGN_SHIFT, _), \ + INCBIN_ALIGNMENT_INDEX) + +/* Stringize */ +#define INCBIN_STR(X) \ + #X +#define INCBIN_STRINGIZE(X) \ + INCBIN_STR(X) +/* Concatenate */ +#define INCBIN_CAT(X, Y) \ + X ## Y +#define INCBIN_CONCATENATE(X, Y) \ + INCBIN_CAT(X, Y) +/* Deferred macro expansion */ +#define INCBIN_EVAL(X) \ + X +#define INCBIN_INVOKE(N, ...) \ + INCBIN_EVAL(N(__VA_ARGS__)) + +/* Green Hills uses a different directive for including binary data */ +#if defined(__ghs__) +# if (__ghs_asm == 2) +# define INCBIN_MACRO ".file" +/* Or consider the ".myrawdata" entry in the ld file */ +# else +# define INCBIN_MACRO "\tINCBIN" +# endif +#else +# define INCBIN_MACRO ".incbin" +#endif + +#ifndef _MSC_VER +# define INCBIN_ALIGN \ + __attribute__((aligned(INCBIN_ALIGNMENT))) +#else +# define INCBIN_ALIGN __declspec(align(INCBIN_ALIGNMENT)) +#endif + +#if defined(__arm__) || /* GNU C and RealView */ \ + defined(__arm) || /* Diab */ \ + defined(_ARM) /* ImageCraft */ +# define INCBIN_ARM +#endif + +#ifdef __GNUC__ +/* Utilize .balign where supported */ +# define INCBIN_ALIGN_HOST ".balign " INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n" +# define INCBIN_ALIGN_BYTE ".balign 1\n" +#elif defined(INCBIN_ARM) +/* + * On arm assemblers, the alignment value is calculated as (1 << n) where `n' is + * the shift count. This is the value passed to `.align' + */ +# define INCBIN_ALIGN_HOST ".align " INCBIN_STRINGIZE(INCBIN_ALIGNMENT_INDEX) "\n" +# define INCBIN_ALIGN_BYTE ".align 0\n" +#else +/* We assume other inline assembler's treat `.align' as `.balign' */ +# define INCBIN_ALIGN_HOST ".align " INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n" +# define INCBIN_ALIGN_BYTE ".align 1\n" +#endif + +/* INCBIN_CONST is used by incbin.c generated files */ +#if defined(__cplusplus) +# define INCBIN_EXTERNAL extern "C" +# define INCBIN_CONST extern const +#else +# define INCBIN_EXTERNAL extern +# define INCBIN_CONST const +#endif + +/** + * @brief Optionally override the linker section into which data is emitted. + * + * @warning If you use this facility, you'll have to deal with platform-specific linker output + * section naming on your own + * + * Overriding the default linker output section, e.g for esp8266/Arduino: + * @code + * #define INCBIN_OUTPUT_SECTION ".irom.text" + * #include "incbin.h" + * INCBIN(Foo, "foo.txt"); + * // Data is emitted into program memory that never gets copied to RAM + * @endcode + */ +#if !defined(INCBIN_OUTPUT_SECTION) +# if defined(__APPLE__) +# define INCBIN_OUTPUT_SECTION ".const_data" +# else +# define INCBIN_OUTPUT_SECTION ".rodata" +# endif +#endif + +#if defined(__APPLE__) +/* The directives are different for Apple branded compilers */ +# define INCBIN_SECTION INCBIN_OUTPUT_SECTION "\n" +# define INCBIN_GLOBAL(NAME) ".globl " INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n" +# define INCBIN_INT ".long " +# define INCBIN_MANGLE "_" +# define INCBIN_BYTE ".byte " +# define INCBIN_TYPE(...) +#else +# define INCBIN_SECTION ".section " INCBIN_OUTPUT_SECTION "\n" +# define INCBIN_GLOBAL(NAME) ".global " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n" +# if defined(__ghs__) +# define INCBIN_INT ".word " +# else +# define INCBIN_INT ".int " +# endif +# if defined(__USER_LABEL_PREFIX__) +# define INCBIN_MANGLE INCBIN_STRINGIZE(__USER_LABEL_PREFIX__) +# else +# define INCBIN_MANGLE "" +# endif +# if defined(INCBIN_ARM) +/* On arm assemblers, `@' is used as a line comment token */ +# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", %object\n" +# elif defined(__MINGW32__) || defined(__MINGW64__) +/* Mingw doesn't support this directive either */ +# define INCBIN_TYPE(NAME) +# else +/* It's safe to use `@' on other architectures */ +# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", @object\n" +# endif +# define INCBIN_BYTE ".byte " +#endif + +/* List of style types used for symbol names */ +#define INCBIN_STYLE_CAMEL 0 +#define INCBIN_STYLE_SNAKE 1 + +/** + * @brief Specify the prefix to use for symbol names. + * + * By default this is `g', producing symbols of the form: + * @code + * #include "incbin.h" + * INCBIN(Foo, "foo.txt"); + * + * // Now you have the following symbols: + * // const unsigned char gFooData[]; + * // const unsigned char *const gFooEnd; + * // const unsigned int gFooSize; + * @endcode + * + * If however you specify a prefix before including: e.g: + * @code + * #define INCBIN_PREFIX incbin + * #include "incbin.h" + * INCBIN(Foo, "foo.txt"); + * + * // Now you have the following symbols instead: + * // const unsigned char incbinFooData[]; + * // const unsigned char *const incbinFooEnd; + * // const unsigned int incbinFooSize; + * @endcode + */ +#if !defined(INCBIN_PREFIX) +# define INCBIN_PREFIX g +#endif + +/** + * @brief Specify the style used for symbol names. + * + * Possible options are + * - INCBIN_STYLE_CAMEL "CamelCase" + * - INCBIN_STYLE_SNAKE "snake_case" + * + * Default option is *INCBIN_STYLE_CAMEL* producing symbols of the form: + * @code + * #include "incbin.h" + * INCBIN(Foo, "foo.txt"); + * + * // Now you have the following symbols: + * // const unsigned char FooData[]; + * // const unsigned char *const FooEnd; + * // const unsigned int FooSize; + * @endcode + * + * If however you specify a style before including: e.g: + * @code + * #define INCBIN_STYLE INCBIN_STYLE_SNAKE + * #include "incbin.h" + * INCBIN(foo, "foo.txt"); + * + * // Now you have the following symbols: + * // const unsigned char foo_data[]; + * // const unsigned char *const foo_end; + * // const unsigned int foo_size; + * @endcode + */ +#if !defined(INCBIN_STYLE) +# define INCBIN_STYLE INCBIN_STYLE_CAMEL +#endif + +/* Style lookup tables */ +#define INCBIN_STYLE_0_DATA Data +#define INCBIN_STYLE_0_END End +#define INCBIN_STYLE_0_SIZE Size +#define INCBIN_STYLE_1_DATA _data +#define INCBIN_STYLE_1_END _end +#define INCBIN_STYLE_1_SIZE _size + +/* Style lookup: returning identifier */ +#define INCBIN_STYLE_IDENT(TYPE) \ + INCBIN_CONCATENATE( \ + INCBIN_STYLE_, \ + INCBIN_CONCATENATE( \ + INCBIN_EVAL(INCBIN_STYLE), \ + INCBIN_CONCATENATE(_, TYPE))) + +/* Style lookup: returning string literal */ +#define INCBIN_STYLE_STRING(TYPE) \ + INCBIN_STRINGIZE( \ + INCBIN_STYLE_IDENT(TYPE)) \ + +/* Generate the global labels by indirectly invoking the macro with our style + * type and concatenating the name against them. */ +#define INCBIN_GLOBAL_LABELS(NAME, TYPE) \ + INCBIN_INVOKE( \ + INCBIN_GLOBAL, \ + INCBIN_CONCATENATE( \ + NAME, \ + INCBIN_INVOKE( \ + INCBIN_STYLE_IDENT, \ + TYPE))) \ + INCBIN_INVOKE( \ + INCBIN_TYPE, \ + INCBIN_CONCATENATE( \ + NAME, \ + INCBIN_INVOKE( \ + INCBIN_STYLE_IDENT, \ + TYPE))) + +/** + * @brief Externally reference binary data included in another translation unit. + * + * Produces three external symbols that reference the binary data included in + * another translation unit. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name given for the binary data + * + * @code + * INCBIN_EXTERN(Foo); + * + * // Now you have the following symbols: + * // extern const unsigned char FooData[]; + * // extern const unsigned char *const FooEnd; + * // extern const unsigned int FooSize; + * @endcode + */ +#define INCBIN_EXTERN(NAME) \ + INCBIN_EXTERNAL const INCBIN_ALIGN unsigned char \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(DATA))[]; \ + INCBIN_EXTERNAL const INCBIN_ALIGN unsigned char *const \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(END)); \ + INCBIN_EXTERNAL const unsigned int \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(SIZE)) + +/** + * @brief Include a binary file into the current translation unit. + * + * Includes a binary file into the current translation unit, producing three symbols + * for objects that encode the data and size respectively. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name to associate with this binary data (as an identifier.) + * @param FILENAME The file to include (as a string literal.) + * + * @code + * INCBIN(Icon, "icon.png"); + * + * // Now you have the following symbols: + * // const unsigned char IconData[]; + * // const unsigned char *const IconEnd; + * // const unsigned int IconSize; + * @endcode + * + * @warning This must be used in global scope + * @warning The identifiers may be different if INCBIN_STYLE is not default + * + * To externally reference the data included by this in another translation unit + * please @see INCBIN_EXTERN. + */ +#ifdef _MSC_VER +#define INCBIN(NAME, FILENAME) \ + INCBIN_EXTERN(NAME) +#else +#define INCBIN(NAME, FILENAME) \ + __asm__(INCBIN_SECTION \ + INCBIN_GLOBAL_LABELS(NAME, DATA) \ + INCBIN_ALIGN_HOST \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) ":\n" \ + INCBIN_MACRO " \"" FILENAME "\"\n" \ + INCBIN_GLOBAL_LABELS(NAME, END) \ + INCBIN_ALIGN_BYTE \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) ":\n" \ + INCBIN_BYTE "1\n" \ + INCBIN_GLOBAL_LABELS(NAME, SIZE) \ + INCBIN_ALIGN_HOST \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(SIZE) ":\n" \ + INCBIN_INT INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) " - " \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) "\n" \ + INCBIN_ALIGN_HOST \ + ".text\n" \ + ); \ + INCBIN_EXTERN(NAME) + +#endif +#endif diff --git a/src/main.cpp b/src/main.cpp index 148bf248f53..fad0ef8449b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,30 +19,32 @@ #include #include "bitboard.h" +#include "endgame.h" #include "position.h" +#include "psqt.h" #include "search.h" +#include "syzygy/tbprobe.h" #include "thread.h" #include "tt.h" #include "uci.h" -#include "endgame.h" -#include "syzygy/tbprobe.h" -namespace PSQT { - void init(); -} +using namespace Stockfish; int main(int argc, char* argv[]) { std::cout << engine_info() << std::endl; + CommandLine::init(argc, argv); UCI::init(Options); + Tune::init(); PSQT::init(); Bitboards::init(); Position::init(); Bitbases::init(); Endgames::init(); - Threads.set(Options["Threads"]); + Threads.set(size_t(Options["Threads"])); Search::clear(); // After threads are up + Eval::NNUE::init(); UCI::loop(argc, argv); diff --git a/src/material.cpp b/src/material.cpp index 0e1308780dd..1567358af48 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,32 +24,39 @@ using namespace std; +namespace Stockfish { + namespace { + #define S(mg, eg) make_score(mg, eg) // Polynomial material imbalance parameters - constexpr int QuadraticOurs[][PIECE_TYPE_NB] = { - // OUR PIECES - // pair pawn knight bishop rook queen - {1438 }, // Bishop pair - { 40, 38 }, // Pawn - { 32, 255, -62 }, // Knight OUR PIECES - { 0, 104, 4, 0 }, // Bishop - { -26, -2, 47, 105, -208 }, // Rook - {-189, 24, 117, 133, -134, -6 } // Queen + // One Score parameter for each pair (our piece, another of our pieces) + constexpr Score QuadraticOurs[][PIECE_TYPE_NB] = { + // OUR PIECE 2 + // bishop pair pawn knight bishop rook queen + {S(1419, 1455) }, // Bishop pair + {S( 101, 28), S( 37, 39) }, // Pawn + {S( 57, 64), S(249, 187), S(-49, -62) }, // Knight OUR PIECE 1 + {S( 0, 0), S(118, 137), S( 10, 27), S( 0, 0) }, // Bishop + {S( -63, -68), S( -5, 3), S(100, 81), S(132, 118), S(-246, -244) }, // Rook + {S(-210, -211), S( 37, 14), S(147, 141), S(161, 105), S(-158, -174), S(-9,-31) } // Queen }; - constexpr int QuadraticTheirs[][PIECE_TYPE_NB] = { - // THEIR PIECES - // pair pawn knight bishop rook queen - { 0 }, // Bishop pair - { 36, 0 }, // Pawn - { 9, 63, 0 }, // Knight OUR PIECES - { 59, 65, 42, 0 }, // Bishop - { 46, 39, 24, -24, 0 }, // Rook - { 97, 100, -42, 137, 268, 0 } // Queen + // One Score parameter for each pair (our piece, their piece) + constexpr Score QuadraticTheirs[][PIECE_TYPE_NB] = { + // THEIR PIECE + // bishop pair pawn knight bishop rook queen + { }, // Bishop pair + {S( 33, 30) }, // Pawn + {S( 46, 18), S(106, 84) }, // Knight OUR PIECE + {S( 75, 35), S( 59, 44), S( 60, 15) }, // Bishop + {S( 26, 35), S( 6, 22), S( 38, 39), S(-12, -2) }, // Rook + {S( 97, 93), S(100, 163), S(-58, -91), S(112, 192), S(276, 225) } // Queen }; + #undef S + // Endgame evaluation and scaling functions are accessed directly and not through // the function maps because they correspond to more than one material hash key. Endgame EvaluateKXK[] = { Endgame(WHITE), Endgame(BLACK) }; @@ -69,7 +74,7 @@ namespace { bool is_KBPsK(const Position& pos, Color us) { return pos.non_pawn_material(us) == BishopValueMg - && pos.count(us) >= 1; + && pos.count(us) >= 1; } bool is_KQKRPs(const Position& pos, Color us) { @@ -79,14 +84,16 @@ namespace { && pos.count(~us) >= 1; } + /// imbalance() calculates the imbalance by comparing the piece count of each /// piece type for both colors. + template - int imbalance(const int pieceCount[][PIECE_TYPE_NB]) { + Score imbalance(const int pieceCount[][PIECE_TYPE_NB]) { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; - int bonus = 0; + Score bonus = SCORE_ZERO; // Second-degree polynomial material imbalance, by Tord Romstad for (int pt1 = NO_PIECE_TYPE; pt1 <= QUEEN; ++pt1) @@ -94,9 +101,9 @@ namespace { if (!pieceCount[Us][pt1]) continue; - int v = 0; + int v = QuadraticOurs[pt1][pt1] * pieceCount[Us][pt1]; - for (int pt2 = NO_PIECE_TYPE; pt2 <= pt1; ++pt2) + for (int pt2 = NO_PIECE_TYPE; pt2 < pt1; ++pt2) v += QuadraticOurs[pt1][pt2] * pieceCount[Us][pt2] + QuadraticTheirs[pt1][pt2] * pieceCount[Them][pt2]; @@ -110,6 +117,7 @@ namespace { namespace Material { + /// Material::probe() looks up the current position's material configuration in /// the material hash table. It returns a pointer to the Entry if the position /// is found. Otherwise a new Entry is computed and stored there, so we don't @@ -129,7 +137,7 @@ Entry* probe(const Position& pos) { Value npm_w = pos.non_pawn_material(WHITE); Value npm_b = pos.non_pawn_material(BLACK); - Value npm = clamp(npm_w + npm_b, EndgameLimit, MidgameLimit); + Value npm = std::clamp(npm_w + npm_b, EndgameLimit, MidgameLimit); // Map total non-pawn material into [PHASE_ENDGAME, PHASE_MIDGAME] e->gamePhase = Phase(((npm - EndgameLimit) * PHASE_MIDGAME) / (MidgameLimit - EndgameLimit)); @@ -212,8 +220,10 @@ Entry* probe(const Position& pos) { { pos.count(BLACK) > 1, pos.count(BLACK), pos.count(BLACK), pos.count(BLACK) , pos.count(BLACK), pos.count(BLACK) } }; - e->value = int16_t((imbalance(pieceCount) - imbalance(pieceCount)) / 16); + e->score = (imbalance(pieceCount) - imbalance(pieceCount)) / 16; return e; } } // namespace Material + +} // namespace Stockfish diff --git a/src/material.h b/src/material.h index 9ab1d81c5e7..3ca169ce0b9 100644 --- a/src/material.h +++ b/src/material.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,7 +24,7 @@ #include "position.h" #include "types.h" -namespace Material { +namespace Stockfish::Material { /// Material::Entry contains various information about a material configuration. /// It contains a material imbalance evaluation, a function pointer to a special @@ -39,12 +37,12 @@ namespace Material { struct Entry { - Score imbalance() const { return make_score(value, value); } - Phase game_phase() const { return gamePhase; } + Score imbalance() const { return score; } + Phase game_phase() const { return (Phase)gamePhase; } bool specialized_eval_exists() const { return evaluationFunction != nullptr; } Value evaluate(const Position& pos) const { return (*evaluationFunction)(pos); } - // scale_factor takes a position and a color as input and returns a scale factor + // scale_factor() takes a position and a color as input and returns a scale factor // for the given color. We have to provide the position in addition to the color // because the scale factor may also be a function which should be applied to // the position. For instance, in KBP vs K endgames, the scaling function looks @@ -59,15 +57,15 @@ struct Entry { const EndgameBase* evaluationFunction; const EndgameBase* scalingFunction[COLOR_NB]; // Could be one for each // side (e.g. KPKP, KBPsK) - int16_t value; + Score score; + int16_t gamePhase; uint8_t factor[COLOR_NB]; - Phase gamePhase; }; typedef HashTable Table; Entry* probe(const Position& pos); -} // namespace Material +} // namespace Stockfish::Material #endif // #ifndef MATERIAL_H_INCLUDED diff --git a/src/misc.cpp b/src/misc.cpp index 202e7e6fea5..41c59b3fd88 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -38,6 +36,8 @@ typedef bool(*fun1_t)(LOGICAL_PROCESSOR_RELATIONSHIP, PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, PDWORD); typedef bool(*fun2_t)(USHORT, PGROUP_AFFINITY); typedef bool(*fun3_t)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY); +typedef bool(*fun4_t)(USHORT, PGROUP_AFFINITY, USHORT, PUSHORT); +typedef WORD(*fun5_t)(); } #endif @@ -46,17 +46,30 @@ typedef bool(*fun3_t)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY); #include #include #include +#include + +#if defined(__linux__) && !defined(__ANDROID__) +#include +#include +#endif + +#if defined(__APPLE__) || defined(__ANDROID__) || defined(__OpenBSD__) || (defined(__GLIBCXX__) && !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_WIN32)) || defined(__e2k__) +#define POSIXALIGNEDALLOC +#include +#endif #include "misc.h" #include "thread.h" using namespace std; +namespace Stockfish { + namespace { /// Version number. If Version is left empty, then compile date in the format /// DD-MM-YY and show in engine_info. -const string Version = "11-CB"; +const string Version = ""; /// Our fancy logging facility. The trick here is to replace cin.rdbuf() and /// cout.rdbuf() with two Tie objects that tie cin and cout to a file stream. We @@ -99,7 +112,14 @@ class Logger { static Logger l; - if (!fname.empty() && !l.file.is_open()) + if (l.file.is_open()) + { + cout.rdbuf(l.out.buf); + cin.rdbuf(l.in.buf); + l.file.close(); + } + + if (!fname.empty()) { l.file.open(fname, ifstream::out); @@ -112,23 +132,18 @@ class Logger { cin.rdbuf(&l.in); cout.rdbuf(&l.out); } - else if (fname.empty() && l.file.is_open()) - { - cout.rdbuf(l.out.buf); - cin.rdbuf(l.in.buf); - l.file.close(); - } } }; } // namespace + /// engine_info() returns the full name of the current Stockfish version. This /// will be either "Stockfish DD-MM-YY" (where DD-MM-YY is the date when /// the program was compiled) or "Stockfish ", depending on whether /// Version is empty. -const string engine_info(bool to_uci) { +string engine_info(bool to_uci) { const string months("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"); string month, day, year; @@ -139,13 +154,11 @@ const string engine_info(bool to_uci) { if (Version.empty()) { date >> month >> day >> year; - ss << "CB-" << setw(2) << year.substr(2) << (1 + months.find(month) / 4) << setw(2) << day; + ss << setw(2) << day << setw(2) << (1 + months.find(month) / 4) << year.substr(2); } - ss << (Is64Bit ? " 64" : "") - << (HasPext ? " BMI2" : (HasPopCnt ? " POPCNT" : "")) - << (to_uci ? "\nid author ": " by ") - << "T. Romstad, M. Costalba, J. Kiiski, G. Linscott"; + ss << (to_uci ? "\nid author ": " by ") + << "the Stockfish developers (see AUTHORS file)"; return ss.str(); } @@ -153,11 +166,11 @@ const string engine_info(bool to_uci) { /// compiler_info() returns a string trying to describe the compiler we use -const std::string compiler_info() { +std::string compiler_info() { - #define STRINGIFY2(x) #x - #define STRINGIFY(x) STRINGIFY2(x) - #define VER_STRING(major, minor, patch) STRINGIFY(major) "." STRINGIFY(minor) "." STRINGIFY(patch) + #define stringify2(x) #x + #define stringify(x) stringify2(x) + #define make_version_string(major, minor, patch) stringify(major) "." stringify(minor) "." stringify(patch) /// Predefined macros hell: /// @@ -171,26 +184,38 @@ const std::string compiler_info() { #ifdef __clang__ compiler += "clang++ "; - compiler += VER_STRING(__clang_major__, __clang_minor__, __clang_patchlevel__); + compiler += make_version_string(__clang_major__, __clang_minor__, __clang_patchlevel__); #elif __INTEL_COMPILER compiler += "Intel compiler "; compiler += "(version "; - compiler += STRINGIFY(__INTEL_COMPILER) " update " STRINGIFY(__INTEL_COMPILER_UPDATE); + compiler += stringify(__INTEL_COMPILER) " update " stringify(__INTEL_COMPILER_UPDATE); compiler += ")"; #elif _MSC_VER compiler += "MSVC "; compiler += "(version "; - compiler += STRINGIFY(_MSC_FULL_VER) "." STRINGIFY(_MSC_BUILD); + compiler += stringify(_MSC_FULL_VER) "." stringify(_MSC_BUILD); + compiler += ")"; + #elif defined(__e2k__) && defined(__LCC__) + #define dot_ver2(n) \ + compiler += (char)'.'; \ + compiler += (char)('0' + (n) / 10); \ + compiler += (char)('0' + (n) % 10); + + compiler += "MCST LCC "; + compiler += "(version "; + compiler += std::to_string(__LCC__ / 100); + dot_ver2(__LCC__ % 100) + dot_ver2(__LCC_MINOR__) compiler += ")"; #elif __GNUC__ compiler += "g++ (GNUC) "; - compiler += VER_STRING(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); + compiler += make_version_string(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); #else compiler += "Unknown compiler "; compiler += "(unknown version)"; #endif - #if defined(__APPLE__) + #if defined(__APPLE__) compiler += " on Apple"; #elif defined(__CYGWIN__) compiler += " on Cygwin"; @@ -210,7 +235,40 @@ const std::string compiler_info() { compiler += " on unknown system"; #endif - compiler += "\n __VERSION__ macro expands to: "; + compiler += "\nCompilation settings include: "; + compiler += (Is64Bit ? " 64bit" : " 32bit"); + #if defined(USE_VNNI) + compiler += " VNNI"; + #endif + #if defined(USE_AVX512) + compiler += " AVX512"; + #endif + compiler += (HasPext ? " BMI2" : ""); + #if defined(USE_AVX2) + compiler += " AVX2"; + #endif + #if defined(USE_SSE41) + compiler += " SSE41"; + #endif + #if defined(USE_SSSE3) + compiler += " SSSE3"; + #endif + #if defined(USE_SSE2) + compiler += " SSE2"; + #endif + compiler += (HasPopCnt ? " POPCNT" : ""); + #if defined(USE_MMX) + compiler += " MMX"; + #endif + #if defined(USE_NEON) + compiler += " NEON"; + #endif + + #if !defined(NDEBUG) + compiler += " DEBUG"; + #endif + + compiler += "\n__VERSION__ macro expands to: "; #ifdef __VERSION__ compiler += __VERSION__; #else @@ -288,6 +346,149 @@ void prefetch(void* addr) { #endif + +/// std_aligned_alloc() is our wrapper for systems where the c++17 implementation +/// does not guarantee the availability of aligned_alloc(). Memory allocated with +/// std_aligned_alloc() must be freed with std_aligned_free(). + +void* std_aligned_alloc(size_t alignment, size_t size) { + +#if defined(POSIXALIGNEDALLOC) + void *mem; + return posix_memalign(&mem, alignment, size) ? nullptr : mem; +#elif defined(_WIN32) + return _mm_malloc(size, alignment); +#else + return std::aligned_alloc(alignment, size); +#endif +} + +void std_aligned_free(void* ptr) { + +#if defined(POSIXALIGNEDALLOC) + free(ptr); +#elif defined(_WIN32) + _mm_free(ptr); +#else + free(ptr); +#endif +} + +/// aligned_large_pages_alloc() will return suitably aligned memory, if possible using large pages. + +#if defined(_WIN32) + +static void* aligned_large_pages_alloc_windows(size_t allocSize) { + + #if !defined(_WIN64) + (void)allocSize; // suppress unused-parameter compiler warning + return nullptr; + #else + + HANDLE hProcessToken { }; + LUID luid { }; + void* mem = nullptr; + + const size_t largePageSize = GetLargePageMinimum(); + if (!largePageSize) + return nullptr; + + // We need SeLockMemoryPrivilege, so try to enable it for the process + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) + return nullptr; + + if (LookupPrivilegeValue(NULL, SE_LOCK_MEMORY_NAME, &luid)) + { + TOKEN_PRIVILEGES tp { }; + TOKEN_PRIVILEGES prevTp { }; + DWORD prevTpLen = 0; + + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // Try to enable SeLockMemoryPrivilege. Note that even if AdjustTokenPrivileges() succeeds, + // we still need to query GetLastError() to ensure that the privileges were actually obtained. + if (AdjustTokenPrivileges( + hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, &prevTpLen) && + GetLastError() == ERROR_SUCCESS) + { + // Round up size to full pages and allocate + allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1); + mem = VirtualAlloc( + NULL, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE); + + // Privilege no longer needed, restore previous state + AdjustTokenPrivileges(hProcessToken, FALSE, &prevTp, 0, NULL, NULL); + } + } + + CloseHandle(hProcessToken); + + return mem; + + #endif +} + +void* aligned_large_pages_alloc(size_t allocSize) { + + // Try to allocate large pages + void* mem = aligned_large_pages_alloc_windows(allocSize); + + // Fall back to regular, page aligned, allocation if necessary + if (!mem) + mem = VirtualAlloc(NULL, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + + return mem; +} + +#else + +void* aligned_large_pages_alloc(size_t allocSize) { + +#if defined(__linux__) + constexpr size_t alignment = 2 * 1024 * 1024; // assumed 2MB page size +#else + constexpr size_t alignment = 4096; // assumed small page size +#endif + + // round up to multiples of alignment + size_t size = ((allocSize + alignment - 1) / alignment) * alignment; + void *mem = std_aligned_alloc(alignment, size); +#if defined(MADV_HUGEPAGE) + madvise(mem, size, MADV_HUGEPAGE); +#endif + return mem; +} + +#endif + + +/// aligned_large_pages_free() will free the previously allocated ttmem + +#if defined(_WIN32) + +void aligned_large_pages_free(void* mem) { + + if (mem && !VirtualFree(mem, 0, MEM_RELEASE)) + { + DWORD err = GetLastError(); + std::cerr << "Failed to free large page memory. Error code: 0x" + << std::hex << err + << std::dec << std::endl; + exit(EXIT_FAILURE); + } +} + +#else + +void aligned_large_pages_free(void *mem) { + std_aligned_free(mem); +} + +#endif + + namespace WinProcGroup { #ifndef _WIN32 @@ -296,11 +497,11 @@ void bindThisThread(size_t) {} #else -/// best_group() retrieves logical processor information using Windows specific -/// API and returns the best group id for the thread with index idx. Original +/// best_node() retrieves logical processor information using Windows specific +/// API and returns the best node id for the thread with index idx. Original /// code from Texel by Peter Österlund. -int best_group(size_t idx) { +int best_node(size_t idx) { int threads = 0; int nodes = 0; @@ -314,7 +515,8 @@ int best_group(size_t idx) { if (!fun1) return -1; - // First call to get returnLength. We expect it to fail due to null buffer + // First call to GetLogicalProcessorInformationEx() to get returnLength. + // We expect the call to fail due to null buffer. if (fun1(RelationAll, nullptr, &returnLength)) return -1; @@ -322,7 +524,7 @@ int best_group(size_t idx) { SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buffer, *ptr; ptr = buffer = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)malloc(returnLength); - // Second call, now we expect to succeed + // Second call to GetLogicalProcessorInformationEx(), now we expect to succeed if (!fun1(RelationAll, buffer, &returnLength)) { free(buffer); @@ -372,24 +574,99 @@ int best_group(size_t idx) { void bindThisThread(size_t idx) { // Use only local variables to be thread-safe - int group = best_group(idx); + int node = best_node(idx); - if (group == -1) + if (node == -1) return; // Early exit if the needed API are not available at runtime HMODULE k32 = GetModuleHandle("Kernel32.dll"); auto fun2 = (fun2_t)(void(*)())GetProcAddress(k32, "GetNumaNodeProcessorMaskEx"); auto fun3 = (fun3_t)(void(*)())GetProcAddress(k32, "SetThreadGroupAffinity"); + auto fun4 = (fun4_t)(void(*)())GetProcAddress(k32, "GetNumaNodeProcessorMask2"); + auto fun5 = (fun5_t)(void(*)())GetProcAddress(k32, "GetMaximumProcessorGroupCount"); if (!fun2 || !fun3) return; - GROUP_AFFINITY affinity; - if (fun2(group, &affinity)) - fun3(GetCurrentThread(), &affinity, nullptr); + if (!fun4 || !fun5) + { + GROUP_AFFINITY affinity; + if (fun2(node, &affinity)) // GetNumaNodeProcessorMaskEx + fun3(GetCurrentThread(), &affinity, nullptr); // SetThreadGroupAffinity + } + else + { + // If a numa node has more than one processor group, we assume they are + // sized equal and we spread threads evenly across the groups. + USHORT elements, returnedElements; + elements = fun5(); // GetMaximumProcessorGroupCount + GROUP_AFFINITY *affinity = (GROUP_AFFINITY*)malloc(elements * sizeof(GROUP_AFFINITY)); + if (fun4(node, affinity, elements, &returnedElements)) // GetNumaNodeProcessorMask2 + fun3(GetCurrentThread(), &affinity[idx % returnedElements], nullptr); // SetThreadGroupAffinity + free(affinity); + } } #endif } // namespace WinProcGroup + +#ifdef _WIN32 +#include +#define GETCWD _getcwd +#else +#include +#define GETCWD getcwd +#endif + +namespace CommandLine { + +string argv0; // path+name of the executable binary, as given by argv[0] +string binaryDirectory; // path of the executable directory +string workingDirectory; // path of the working directory + +void init(int argc, char* argv[]) { + (void)argc; + string pathSeparator; + + // extract the path+name of the executable binary + argv0 = argv[0]; + +#ifdef _WIN32 + pathSeparator = "\\"; + #ifdef _MSC_VER + // Under windows argv[0] may not have the extension. Also _get_pgmptr() had + // issues in some windows 10 versions, so check returned values carefully. + char* pgmptr = nullptr; + if (!_get_pgmptr(&pgmptr) && pgmptr != nullptr && *pgmptr) + argv0 = pgmptr; + #endif +#else + pathSeparator = "/"; +#endif + + // extract the working directory + workingDirectory = ""; + char buff[40000]; + char* cwd = GETCWD(buff, 40000); + if (cwd) + workingDirectory = cwd; + + // extract the binary directory path from argv0 + binaryDirectory = argv0; + size_t pos = binaryDirectory.find_last_of("\\/"); + if (pos == std::string::npos) + binaryDirectory = "." + pathSeparator; + else + binaryDirectory.resize(pos + 1); + + // pattern replacement: "./" at the start of path is replaced by the working directory + if (binaryDirectory.find("." + pathSeparator) == 0) + binaryDirectory.replace(0, 1, workingDirectory); +} + + +} // namespace CommandLine + +} // namespace Stockfish diff --git a/src/misc.h b/src/misc.h index b11c5aa843c..b962673384b 100644 --- a/src/misc.h +++ b/src/misc.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,13 +24,20 @@ #include #include #include +#include #include "types.h" -const std::string engine_info(bool to_uci = false); -const std::string compiler_info(); +namespace Stockfish { + +std::string engine_info(bool to_uci = false); +std::string compiler_info(); void prefetch(void* addr); void start_logger(const std::string& fname); +void* std_aligned_alloc(size_t alignment, size_t size); +void std_aligned_free(void* ptr); +void* aligned_large_pages_alloc(size_t size); // memory aligned by page size, min alignment: 4096 bytes +void aligned_large_pages_free(void* mem); // nop if mem == nullptr void dbg_hit_on(bool b); void dbg_hit_on(bool c, bool b); @@ -40,9 +45,7 @@ void dbg_mean_of(int v); void dbg_print(); typedef std::chrono::milliseconds::rep TimePoint; // A value in milliseconds - static_assert(sizeof(TimePoint) == sizeof(int64_t), "TimePoint should be 64 bits"); - inline TimePoint now() { return std::chrono::duration_cast (std::chrono::steady_clock::now().time_since_epoch()).count(); @@ -64,6 +67,107 @@ std::ostream& operator<<(std::ostream&, SyncCout); #define sync_endl std::endl << IO_UNLOCK +// align_ptr_up() : get the first aligned element of an array. +// ptr must point to an array of size at least `sizeof(T) * N + alignment` bytes, +// where N is the number of elements in the array. +template +T* align_ptr_up(T* ptr) +{ + static_assert(alignof(T) < Alignment); + + const uintptr_t ptrint = reinterpret_cast(reinterpret_cast(ptr)); + return reinterpret_cast(reinterpret_cast((ptrint + (Alignment - 1)) / Alignment * Alignment)); +} + + +// IsLittleEndian : true if and only if the binary is compiled on a little endian machine +static inline const union { uint32_t i; char c[4]; } Le = { 0x01020304 }; +static inline const bool IsLittleEndian = (Le.c[0] == 4); + + +// RunningAverage : a class to calculate a running average of a series of values. +// For efficiency, all computations are done with integers. +class RunningAverage { + public: + + // Constructor + RunningAverage() {} + + // Reset the running average to rational value p / q + void set(int64_t p, int64_t q) + { average = p * PERIOD * RESOLUTION / q; } + + // Update average with value v + void update(int64_t v) + { average = RESOLUTION * v + (PERIOD - 1) * average / PERIOD; } + + // Test if average is strictly greater than rational a / b + bool is_greater(int64_t a, int64_t b) + { return b * average > a * PERIOD * RESOLUTION ; } + + int64_t value() + { return average / (PERIOD * RESOLUTION); } + + private : + static constexpr int64_t PERIOD = 4096; + static constexpr int64_t RESOLUTION = 1024; + int64_t average; +}; + +template +class ValueList { + +public: + std::size_t size() const { return size_; } + void resize(std::size_t newSize) { size_ = newSize; } + void push_back(const T& value) { values_[size_++] = value; } + T& operator[](std::size_t index) { return values_[index]; } + T* begin() { return values_; } + T* end() { return values_ + size_; } + const T& operator[](std::size_t index) const { return values_[index]; } + const T* begin() const { return values_; } + const T* end() const { return values_ + size_; } + + void swap(ValueList& other) { + const std::size_t maxSize = std::max(size_, other.size_); + for (std::size_t i = 0; i < maxSize; ++i) { + std::swap(values_[i], other.values_[i]); + } + std::swap(size_, other.size_); + } + +private: + T values_[MaxSize]; + std::size_t size_ = 0; +}; + + +/// sigmoid(t, x0, y0, C, P, Q) implements a sigmoid-like function using only integers, +/// with the following properties: +/// +/// - sigmoid is centered in (x0, y0) +/// - sigmoid has amplitude [-P/Q , P/Q] instead of [-1 , +1] +/// - limit is (y0 - P/Q) when t tends to -infinity +/// - limit is (y0 + P/Q) when t tends to +infinity +/// - the slope can be adjusted using C > 0, smaller C giving a steeper sigmoid +/// - the slope of the sigmoid when t = x0 is P/(Q*C) +/// - sigmoid is increasing with t when P > 0 and Q > 0 +/// - to get a decreasing sigmoid, call with -t, or change sign of P +/// - mean value of the sigmoid is y0 +/// +/// Use to draw the sigmoid + +inline int64_t sigmoid(int64_t t, int64_t x0, + int64_t y0, + int64_t C, + int64_t P, + int64_t Q) +{ + assert(C > 0); + return y0 + P * (t-x0) / (Q * (std::abs(t-x0) + C)) ; +} + + /// xorshift64star Pseudo-Random Number Generator /// This class is based on original code written and dedicated /// to the public domain by Sebastiano Vigna (2014). @@ -100,6 +204,19 @@ class PRNG { { return T(rand64() & rand64() & rand64()); } }; +inline uint64_t mul_hi64(uint64_t a, uint64_t b) { +#if defined(__GNUC__) && defined(IS_64BIT) + __extension__ typedef unsigned __int128 uint128; + return ((uint128)a * (uint128)b) >> 64; +#else + uint64_t aL = (uint32_t)a, aH = a >> 32; + uint64_t bL = (uint32_t)b, bH = b >> 32; + uint64_t c1 = (aL * bL) >> 32; + uint64_t c2 = aH * bL + c1; + uint64_t c3 = aL * bH + (uint32_t)c2; + return aH * bH + (c2 >> 32) + (c3 >> 32); +#endif +} /// Under Windows it is not possible for a process to run on more than one /// logical processor group. This usually means to be limited to use max 64 @@ -111,4 +228,13 @@ namespace WinProcGroup { void bindThisThread(size_t idx); } +namespace CommandLine { + void init(int argc, char* argv[]); + + extern std::string binaryDirectory; // path of the executable directory + extern std::string workingDirectory; // path of the working directory +} + +} // namespace Stockfish + #endif // #ifndef MISC_H_INCLUDED diff --git a/src/movegen.cpp b/src/movegen.cpp index 8f6edffbfc3..c7a3c29bc04 100644 --- a/src/movegen.cpp +++ b/src/movegen.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,10 +21,12 @@ #include "movegen.h" #include "position.h" +namespace Stockfish { + namespace { template - ExtMove* make_promotions(ExtMove* moveList, Square to, Square ksq) { + ExtMove* make_promotions(ExtMove* moveList, Square to) { if (Type == CAPTURES || Type == EVASIONS || Type == NON_EVASIONS) *moveList++ = make(to - D, to, QUEEN); @@ -38,13 +38,6 @@ namespace { *moveList++ = make(to - D, to, KNIGHT); } - // Knight promotion is the only promotion that can give a direct check - // that's not already included in the queen promotion. - if (Type == QUIET_CHECKS && (PseudoAttacks[KNIGHT][to] & ksq)) - *moveList++ = make(to - D, to, KNIGHT); - else - (void)ksq; // Silence a warning under MSVC - return moveList; } @@ -52,28 +45,23 @@ namespace { template ExtMove* generate_pawn_moves(const Position& pos, ExtMove* moveList, Bitboard target) { - // Compute some compile time parameters relative to the white side - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Bitboard TRank7BB = (Us == WHITE ? Rank7BB : Rank2BB); constexpr Bitboard TRank3BB = (Us == WHITE ? Rank3BB : Rank6BB); constexpr Direction Up = pawn_push(Us); constexpr Direction UpRight = (Us == WHITE ? NORTH_EAST : SOUTH_WEST); constexpr Direction UpLeft = (Us == WHITE ? NORTH_WEST : SOUTH_EAST); - const Square ksq = pos.square(Them); - Bitboard emptySquares; + const Bitboard emptySquares = ~pos.pieces(); + const Bitboard enemies = Type == EVASIONS ? pos.checkers() + : pos.pieces(Them); Bitboard pawnsOn7 = pos.pieces(Us, PAWN) & TRank7BB; Bitboard pawnsNotOn7 = pos.pieces(Us, PAWN) & ~TRank7BB; - Bitboard enemies = (Type == EVASIONS ? pos.pieces(Them) & target: - Type == CAPTURES ? target : pos.pieces(Them)); - // Single and double pawn pushes, no promotions if (Type != CAPTURES) { - emptySquares = (Type == QUIETS || Type == QUIET_CHECKS ? target : ~pos.pieces()); - Bitboard b1 = shift(pawnsNotOn7) & emptySquares; Bitboard b2 = shift(b1 & TRank3BB) & emptySquares; @@ -85,33 +73,24 @@ namespace { if (Type == QUIET_CHECKS) { - b1 &= pos.attacks_from(ksq, Them); - b2 &= pos.attacks_from(ksq, Them); - - // Add pawn pushes which give discovered check. This is possible only - // if the pawn is not on the same file as the enemy king, because we - // don't generate captures. Note that a possible discovery check - // promotion has been already generated amongst the captures. - Bitboard dcCandidateQuiets = pos.blockers_for_king(Them) & pawnsNotOn7; - if (dcCandidateQuiets) - { - Bitboard dc1 = shift(dcCandidateQuiets) & emptySquares & ~file_bb(ksq); - Bitboard dc2 = shift(dc1 & TRank3BB) & emptySquares; - - b1 |= dc1; - b2 |= dc2; - } + // To make a quiet check, you either make a direct check by pushing a pawn + // or push a blocker pawn that is not on the same file as the enemy king. + // Discovered check promotion has been already generated amongst the captures. + Square ksq = pos.square(Them); + Bitboard dcCandidatePawns = pos.blockers_for_king(Them) & ~file_bb(ksq); + b1 &= pawn_attacks_bb(Them, ksq) | shift< Up>(dcCandidatePawns); + b2 &= pawn_attacks_bb(Them, ksq) | shift(dcCandidatePawns); } while (b1) { - Square to = pop_lsb(&b1); + Square to = pop_lsb(b1); *moveList++ = make_move(to - Up, to); } while (b2) { - Square to = pop_lsb(&b2); + Square to = pop_lsb(b2); *moveList++ = make_move(to - Up - Up, to); } } @@ -119,27 +98,24 @@ namespace { // Promotions and underpromotions if (pawnsOn7) { - if (Type == CAPTURES) - emptySquares = ~pos.pieces(); - - if (Type == EVASIONS) - emptySquares &= target; - Bitboard b1 = shift(pawnsOn7) & enemies; Bitboard b2 = shift(pawnsOn7) & enemies; Bitboard b3 = shift(pawnsOn7) & emptySquares; + if (Type == EVASIONS) + b3 &= target; + while (b1) - moveList = make_promotions(moveList, pop_lsb(&b1), ksq); + moveList = make_promotions(moveList, pop_lsb(b1)); while (b2) - moveList = make_promotions(moveList, pop_lsb(&b2), ksq); + moveList = make_promotions(moveList, pop_lsb(b2)); while (b3) - moveList = make_promotions(moveList, pop_lsb(&b3), ksq); + moveList = make_promotions(moveList, pop_lsb(b3)); } - // Standard and en-passant captures + // Standard and en passant captures if (Type == CAPTURES || Type == EVASIONS || Type == NON_EVASIONS) { Bitboard b1 = shift(pawnsNotOn7) & enemies; @@ -147,13 +123,13 @@ namespace { while (b1) { - Square to = pop_lsb(&b1); + Square to = pop_lsb(b1); *moveList++ = make_move(to - UpRight, to); } while (b2) { - Square to = pop_lsb(&b2); + Square to = pop_lsb(b2); *moveList++ = make_move(to - UpLeft, to); } @@ -161,18 +137,16 @@ namespace { { assert(rank_of(pos.ep_square()) == relative_rank(Us, RANK_6)); - // An en passant capture can be an evasion only if the checking piece - // is the double pushed pawn and so is in the target. Otherwise this - // is a discovery check and we are forced to do otherwise. - if (Type == EVASIONS && !(target & (pos.ep_square() - Up))) + // An en passant capture cannot resolve a discovered check + if (Type == EVASIONS && (target & (pos.ep_square() + Up))) return moveList; - b1 = pawnsNotOn7 & pos.attacks_from(pos.ep_square(), Them); + b1 = pawnsNotOn7 & pawn_attacks_bb(Them, pos.ep_square()); assert(b1); while (b1) - *moveList++ = make(pop_lsb(&b1), pos.ep_square()); + *moveList++ = make(pop_lsb(b1), pos.ep_square()); } } @@ -180,33 +154,24 @@ namespace { } - template - ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Color us, - Bitboard target) { + template + ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) { static_assert(Pt != KING && Pt != PAWN, "Unsupported piece type in generate_moves()"); - const Square* pl = pos.squares(us); + Bitboard bb = pos.pieces(Us, Pt); - for (Square from = *pl; from != SQ_NONE; from = *++pl) + while (bb) { - if (Checks) - { - if ( (Pt == BISHOP || Pt == ROOK || Pt == QUEEN) - && !(PseudoAttacks[Pt][from] & target & pos.check_squares(Pt))) - continue; - - if (pos.blockers_for_king(~us) & from) - continue; - } + Square from = pop_lsb(bb); + Bitboard b = attacks_bb(from, pos.pieces()) & target; - Bitboard b = pos.attacks_from(from) & target; - - if (Checks) + // To check, you either move freely a blocker or make a direct check. + if (Checks && (Pt == QUEEN || !(pos.blockers_for_king(~Us) & from))) b &= pos.check_squares(Pt); while (b) - *moveList++ = make_move(from, pop_lsb(&b)); + *moveList++ = make_move(from, pop_lsb(b)); } return moveList; @@ -214,33 +179,42 @@ namespace { template - ExtMove* generate_all(const Position& pos, ExtMove* moveList, Bitboard target) { + ExtMove* generate_all(const Position& pos, ExtMove* moveList) { - constexpr CastlingRights OO = Us & KING_SIDE; - constexpr CastlingRights OOO = Us & QUEEN_SIDE; - constexpr bool Checks = Type == QUIET_CHECKS; // Reduce template instantations + static_assert(Type != LEGAL, "Unsupported type in generate_all()"); - moveList = generate_pawn_moves(pos, moveList, target); - moveList = generate_moves(pos, moveList, Us, target); - moveList = generate_moves(pos, moveList, Us, target); - moveList = generate_moves< ROOK, Checks>(pos, moveList, Us, target); - moveList = generate_moves< QUEEN, Checks>(pos, moveList, Us, target); + constexpr bool Checks = Type == QUIET_CHECKS; // Reduce template instantiations + const Square ksq = pos.square(Us); + Bitboard target; - if (Type != QUIET_CHECKS && Type != EVASIONS) + // Skip generating non-king moves when in double check + if (Type != EVASIONS || !more_than_one(pos.checkers())) { - Square ksq = pos.square(Us); - Bitboard b = pos.attacks_from(ksq) & target; - while (b) - *moveList++ = make_move(ksq, pop_lsb(&b)); + target = Type == EVASIONS ? between_bb(ksq, lsb(pos.checkers())) + : Type == NON_EVASIONS ? ~pos.pieces( Us) + : Type == CAPTURES ? pos.pieces(~Us) + : ~pos.pieces( ); // QUIETS || QUIET_CHECKS + + moveList = generate_pawn_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + moveList = generate_moves(pos, moveList, target); + } - if (Type != CAPTURES && pos.can_castle(CastlingRights(OO | OOO))) - { - if (!pos.castling_impeded(OO) && pos.can_castle(OO)) - *moveList++ = make(ksq, pos.castling_rook_square(OO)); + if (!Checks || pos.blockers_for_king(~Us) & ksq) + { + Bitboard b = attacks_bb(ksq) & (Type == EVASIONS ? ~pos.pieces(Us) : target); + if (Checks) + b &= ~attacks_bb(pos.square(~Us)); - if (!pos.castling_impeded(OOO) && pos.can_castle(OOO)) - *moveList++ = make(ksq, pos.castling_rook_square(OOO)); - } + while (b) + *moveList++ = make_move(ksq, pop_lsb(b)); + + if ((Type == QUIETS || Type == NON_EVASIONS) && pos.can_castle(Us & ANY_CASTLING)) + for (CastlingRights cr : { Us & KING_SIDE, Us & QUEEN_SIDE } ) + if (!pos.castling_impeded(cr) && pos.can_castle(cr)) + *moveList++ = make(ksq, pos.castling_rook_square(cr)); } return moveList; @@ -249,8 +223,10 @@ namespace { } // namespace -/// Generates all pseudo-legal captures and queen promotions +/// Generates all pseudo-legal captures plus queen promotions /// Generates all pseudo-legal non-captures and underpromotions +/// Generates all pseudo-legal check evasions when the side to move is in check +/// Generates all pseudo-legal non-captures giving check, except castling and promotions /// Generates all pseudo-legal captures and non-captures /// /// Returns a pointer to the end of the move list. @@ -258,95 +234,23 @@ namespace { template ExtMove* generate(const Position& pos, ExtMove* moveList) { - static_assert(Type == CAPTURES || Type == QUIETS || Type == NON_EVASIONS, "Unsupported type in generate()"); - assert(!pos.checkers()); + static_assert(Type != LEGAL, "Unsupported type in generate()"); + assert((Type == EVASIONS) == (bool)pos.checkers()); Color us = pos.side_to_move(); - Bitboard target = Type == CAPTURES ? pos.pieces(~us) - : Type == QUIETS ? ~pos.pieces() - : Type == NON_EVASIONS ? ~pos.pieces(us) : 0; - - return us == WHITE ? generate_all(pos, moveList, target) - : generate_all(pos, moveList, target); + return us == WHITE ? generate_all(pos, moveList) + : generate_all(pos, moveList); } // Explicit template instantiations template ExtMove* generate(const Position&, ExtMove*); template ExtMove* generate(const Position&, ExtMove*); +template ExtMove* generate(const Position&, ExtMove*); +template ExtMove* generate(const Position&, ExtMove*); template ExtMove* generate(const Position&, ExtMove*); -/// generate generates all pseudo-legal non-captures and knight -/// underpromotions that give check. Returns a pointer to the end of the move list. -template<> -ExtMove* generate(const Position& pos, ExtMove* moveList) { - - assert(!pos.checkers()); - - Color us = pos.side_to_move(); - Bitboard dc = pos.blockers_for_king(~us) & pos.pieces(us); - - while (dc) - { - Square from = pop_lsb(&dc); - PieceType pt = type_of(pos.piece_on(from)); - - if (pt == PAWN) - continue; // Will be generated together with direct checks - - Bitboard b = pos.attacks_from(pt, from) & ~pos.pieces(); - - if (pt == KING) - b &= ~PseudoAttacks[QUEEN][pos.square(~us)]; - - while (b) - *moveList++ = make_move(from, pop_lsb(&b)); - } - - return us == WHITE ? generate_all(pos, moveList, ~pos.pieces()) - : generate_all(pos, moveList, ~pos.pieces()); -} - - -/// generate generates all pseudo-legal check evasions when the side -/// to move is in check. Returns a pointer to the end of the move list. -template<> -ExtMove* generate(const Position& pos, ExtMove* moveList) { - - assert(pos.checkers()); - - Color us = pos.side_to_move(); - Square ksq = pos.square(us); - Bitboard sliderAttacks = 0; - Bitboard sliders = pos.checkers() & ~pos.pieces(KNIGHT, PAWN); - - // Find all the squares attacked by slider checkers. We will remove them from - // the king evasions in order to skip known illegal moves, which avoids any - // useless legality checks later on. - while (sliders) - { - Square checksq = pop_lsb(&sliders); - sliderAttacks |= LineBB[checksq][ksq] ^ checksq; - } - - // Generate evasions for king, capture and non capture moves - Bitboard b = pos.attacks_from(ksq) & ~pos.pieces(us) & ~sliderAttacks; - while (b) - *moveList++ = make_move(ksq, pop_lsb(&b)); - - if (more_than_one(pos.checkers())) - return moveList; // Double check, only a king move can save the day - - // Generate blocking evasions or captures of the checking piece - Square checksq = lsb(pos.checkers()); - Bitboard target = between_bb(checksq, ksq) | checksq; - - return us == WHITE ? generate_all(pos, moveList, target) - : generate_all(pos, moveList, target); -} - - /// generate generates all the legal moves in the given position template<> @@ -360,7 +264,7 @@ ExtMove* generate(const Position& pos, ExtMove* moveList) { moveList = pos.checkers() ? generate(pos, moveList) : generate(pos, moveList); while (cur != moveList) - if ( (pinned || from_sq(*cur) == ksq || type_of(*cur) == ENPASSANT) + if ( ((pinned && pinned & from_sq(*cur)) || from_sq(*cur) == ksq || type_of(*cur) == EN_PASSANT) && !pos.legal(*cur)) *cur = (--moveList)->move; else @@ -368,3 +272,5 @@ ExtMove* generate(const Position& pos, ExtMove* moveList) { return moveList; } + +} // namespace Stockfish diff --git a/src/movegen.h b/src/movegen.h index c2e7c3f1101..bbb35b39159 100644 --- a/src/movegen.h +++ b/src/movegen.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,6 +23,8 @@ #include "types.h" +namespace Stockfish { + class Position; enum GenType { @@ -72,4 +72,6 @@ struct MoveList { ExtMove moveList[MAX_MOVES], *last; }; +} // namespace Stockfish + #endif // #ifndef MOVEGEN_H_INCLUDED diff --git a/src/movepick.cpp b/src/movepick.cpp index 025f5b82cde..694b9222fe5 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +20,8 @@ #include "movepick.h" +namespace Stockfish { + namespace { enum Stages { @@ -57,44 +57,44 @@ namespace { /// MovePicker constructor for the main search MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const ButterflyHistory* mh, - const CapturePieceToHistory* cph, const PieceToHistory** ch, Move cm, Move* killers) + const CapturePieceToHistory* cph, + const PieceToHistory** ch, + Move cm, + const Move* killers) : pos(p), mainHistory(mh), captureHistory(cph), continuationHistory(ch), - refutations{{killers[0], 0}, {killers[1], 0}, {cm, 0}}, depth(d) { - + ttMove(ttm), refutations{{killers[0], 0}, {killers[1], 0}, {cm, 0}}, depth(d) +{ assert(d > 0); - stage = pos.checkers() ? EVASION_TT : MAIN_TT; - ttMove = ttm && pos.pseudo_legal(ttm) ? ttm : MOVE_NONE; - stage += (ttMove == MOVE_NONE); + stage = (pos.checkers() ? EVASION_TT : MAIN_TT) + + !(ttm && pos.pseudo_legal(ttm)); } /// MovePicker constructor for quiescence search MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const ButterflyHistory* mh, - const CapturePieceToHistory* cph, const PieceToHistory** ch, Square rs) - : pos(p), mainHistory(mh), captureHistory(cph), continuationHistory(ch), recaptureSquare(rs), depth(d) { - + const CapturePieceToHistory* cph, + const PieceToHistory** ch, + Square rs) + : pos(p), mainHistory(mh), captureHistory(cph), continuationHistory(ch), ttMove(ttm), recaptureSquare(rs), depth(d) +{ assert(d <= 0); - stage = pos.checkers() ? EVASION_TT : QSEARCH_TT; - ttMove = ttm - && (depth > DEPTH_QS_RECAPTURES || to_sq(ttm) == recaptureSquare) - && pos.pseudo_legal(ttm) ? ttm : MOVE_NONE; - stage += (ttMove == MOVE_NONE); + stage = (pos.checkers() ? EVASION_TT : QSEARCH_TT) + + !( ttm + && (pos.checkers() || depth > DEPTH_QS_RECAPTURES || to_sq(ttm) == recaptureSquare) + && pos.pseudo_legal(ttm)); } /// MovePicker constructor for ProbCut: we generate captures with SEE greater /// than or equal to the given threshold. MovePicker::MovePicker(const Position& p, Move ttm, Value th, const CapturePieceToHistory* cph) - : pos(p), captureHistory(cph), threshold(th) { - + : pos(p), captureHistory(cph), ttMove(ttm), threshold(th) +{ assert(!pos.checkers()); - stage = PROBCUT_TT; - ttMove = ttm - && pos.capture(ttm) - && pos.pseudo_legal(ttm) - && pos.see_ge(ttm, threshold) ? ttm : MOVE_NONE; - stage += (ttMove == MOVE_NONE); + stage = PROBCUT_TT + !(ttm && pos.capture(ttm) + && pos.pseudo_legal(ttm) + && pos.see_ge(ttm, threshold)); } /// MovePicker::score() assigns a numerical value to each move in a list, used @@ -106,15 +106,15 @@ void MovePicker::score() { static_assert(Type == CAPTURES || Type == QUIETS || Type == EVASIONS, "Wrong type"); for (auto& m : *this) - if (Type == CAPTURES) + if constexpr (Type == CAPTURES) m.value = int(PieceValue[MG][pos.piece_on(to_sq(m))]) * 6 + (*captureHistory)[pos.moved_piece(m)][to_sq(m)][type_of(pos.piece_on(to_sq(m)))]; - else if (Type == QUIETS) + else if constexpr (Type == QUIETS) m.value = (*mainHistory)[pos.side_to_move()][from_to(m)] + 2 * (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)] - + 2 * (*continuationHistory[1])[pos.moved_piece(m)][to_sq(m)] - + 2 * (*continuationHistory[3])[pos.moved_piece(m)][to_sq(m)] + + (*continuationHistory[1])[pos.moved_piece(m)][to_sq(m)] + + (*continuationHistory[3])[pos.moved_piece(m)][to_sq(m)] + (*continuationHistory[5])[pos.moved_piece(m)][to_sq(m)]; else // Type == EVASIONS @@ -123,8 +123,8 @@ void MovePicker::score() { m.value = PieceValue[MG][pos.piece_on(to_sq(m))] - Value(type_of(pos.moved_piece(m))); else - m.value = (*mainHistory)[pos.side_to_move()][from_to(m)] - + (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)] + m.value = (*mainHistory)[pos.side_to_move()][from_to(m)] + + 2 * (*continuationHistory[0])[pos.moved_piece(m)][to_sq(m)] - (1 << 28); } } @@ -148,7 +148,7 @@ Move MovePicker::select(Pred filter) { } /// MovePicker::next_move() is the most important method of the MovePicker class. It -/// returns a new pseudo legal move every time it is called until there are no more +/// returns a new pseudo-legal move every time it is called until there are no more /// moves left, picking the move with the highest score from a list of generated moves. Move MovePicker::next_move(bool skipQuiets) { @@ -174,7 +174,7 @@ Move MovePicker::next_move(bool skipQuiets) { case GOOD_CAPTURE: if (select([&](){ - return pos.see_ge(*cur, Value(-55 * cur->value / 1024)) ? + return pos.see_ge(*cur, Value(-69 * cur->value / 1024)) ? // Move losing capture to endBadCaptures to be tried later true : (*endBadCaptures++ = *cur, false); })) return *(cur - 1); @@ -189,7 +189,7 @@ Move MovePicker::next_move(bool skipQuiets) { --endMoves; ++stage; - /* fallthrough */ + [[fallthrough]]; case REFUTATION: if (select([&](){ return *cur != MOVE_NONE @@ -197,7 +197,7 @@ Move MovePicker::next_move(bool skipQuiets) { && pos.pseudo_legal(*cur); })) return *(cur - 1); ++stage; - /* fallthrough */ + [[fallthrough]]; case QUIET_INIT: if (!skipQuiets) @@ -210,7 +210,7 @@ Move MovePicker::next_move(bool skipQuiets) { } ++stage; - /* fallthrough */ + [[fallthrough]]; case QUIET: if ( !skipQuiets @@ -224,7 +224,7 @@ Move MovePicker::next_move(bool skipQuiets) { endMoves = endBadCaptures; ++stage; - /* fallthrough */ + [[fallthrough]]; case BAD_CAPTURE: return select([](){ return true; }); @@ -235,7 +235,7 @@ Move MovePicker::next_move(bool skipQuiets) { score(); ++stage; - /* fallthrough */ + [[fallthrough]]; case EVASION: return select([](){ return true; }); @@ -253,14 +253,14 @@ Move MovePicker::next_move(bool skipQuiets) { return MOVE_NONE; ++stage; - /* fallthrough */ + [[fallthrough]]; case QCHECK_INIT: cur = moves; endMoves = generate(pos, cur); ++stage; - /* fallthrough */ + [[fallthrough]]; case QCHECK: return select([](){ return true; }); @@ -269,3 +269,5 @@ Move MovePicker::next_move(bool skipQuiets) { assert(false); return MOVE_NONE; // Silence warning } + +} // namespace Stockfish diff --git a/src/movepick.h b/src/movepick.h index cdedc9b616f..e2cbfcdee33 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -29,6 +27,8 @@ #include "position.h" #include "types.h" +namespace Stockfish { + /// StatsEntry stores the stat table value. It is usually a number but could /// be a move or even a nested history. We use a class instead of naked value /// to directly call history update operator<<() on the entry so to use stats @@ -86,7 +86,7 @@ enum StatsType { NoCaptures, Captures }; /// unsuccessful during the current search, and is used for reduction and move /// ordering decisions. It uses 2 tables (one for each color) indexed by /// the move's from and to squares, see www.chessprogramming.org/Butterfly_Boards -typedef Stats ButterflyHistory; +typedef Stats ButterflyHistory; /// CounterMoveHistory stores counter moves indexed by [piece][to] of the previous /// move, see www.chessprogramming.org/Countermove_Heuristic @@ -104,12 +104,12 @@ typedef Stats PieceToHistory; typedef Stats ContinuationHistory; -/// MovePicker class is used to pick one pseudo legal move at a time from the +/// MovePicker class is used to pick one pseudo-legal move at a time from the /// current position. The most important method is next_move(), which returns a -/// new pseudo legal move each time it is called, until there are no moves left, -/// when MOVE_NONE is returned. In order to improve the efficiency of the alpha -/// beta algorithm, MovePicker attempts to return the moves which are most likely -/// to get a cut-off first. +/// new pseudo-legal move each time it is called, until there are no moves left, +/// when MOVE_NONE is returned. In order to improve the efficiency of the +/// alpha-beta algorithm, MovePicker attempts to return the moves which are most +/// likely to get a cut-off first. class MovePicker { enum PickType { Next, Best }; @@ -117,16 +117,16 @@ class MovePicker { public: MovePicker(const MovePicker&) = delete; MovePicker& operator=(const MovePicker&) = delete; - MovePicker(const Position&, Move, Value, const CapturePieceToHistory*); MovePicker(const Position&, Move, Depth, const ButterflyHistory*, const CapturePieceToHistory*, const PieceToHistory**, - Square); + Move, + const Move*); MovePicker(const Position&, Move, Depth, const ButterflyHistory*, const CapturePieceToHistory*, const PieceToHistory**, - Move, - Move*); + Square); + MovePicker(const Position&, Move, Value, const CapturePieceToHistory*); Move next_move(bool skipQuiets = false); private: @@ -148,4 +148,6 @@ class MovePicker { ExtMove moves[MAX_MOVES]; }; +} // namespace Stockfish + #endif // #ifndef MOVEPICK_H_INCLUDED diff --git a/src/nnue/evaluate_nnue.cpp b/src/nnue/evaluate_nnue.cpp new file mode 100644 index 00000000000..862b2003388 --- /dev/null +++ b/src/nnue/evaluate_nnue.cpp @@ -0,0 +1,414 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Code for calculating NNUE evaluation function + +#include +#include +#include +#include +#include + +#include "../evaluate.h" +#include "../position.h" +#include "../misc.h" +#include "../uci.h" +#include "../types.h" + +#include "evaluate_nnue.h" + +namespace Stockfish::Eval::NNUE { + + // Input feature converter + LargePagePtr featureTransformer; + + // Evaluation function + AlignedPtr network[LayerStacks]; + + // Evaluation function file name + std::string fileName; + std::string netDescription; + + namespace Detail { + + // Initialize the evaluation function parameters + template + void initialize(AlignedPtr& pointer) { + + pointer.reset(reinterpret_cast(std_aligned_alloc(alignof(T), sizeof(T)))); + std::memset(pointer.get(), 0, sizeof(T)); + } + + template + void initialize(LargePagePtr& pointer) { + + static_assert(alignof(T) <= 4096, "aligned_large_pages_alloc() may fail for such a big alignment requirement of T"); + pointer.reset(reinterpret_cast(aligned_large_pages_alloc(sizeof(T)))); + std::memset(pointer.get(), 0, sizeof(T)); + } + + // Read evaluation function parameters + template + bool read_parameters(std::istream& stream, T& reference) { + + std::uint32_t header; + header = read_little_endian(stream); + if (!stream || header != T::get_hash_value()) return false; + return reference.read_parameters(stream); + } + + // Write evaluation function parameters + template + bool write_parameters(std::ostream& stream, const T& reference) { + + write_little_endian(stream, T::get_hash_value()); + return reference.write_parameters(stream); + } + + } // namespace Detail + + // Initialize the evaluation function parameters + void initialize() { + + Detail::initialize(featureTransformer); + for (std::size_t i = 0; i < LayerStacks; ++i) + Detail::initialize(network[i]); + } + + // Read network header + bool read_header(std::istream& stream, std::uint32_t* hashValue, std::string* desc) + { + std::uint32_t version, size; + + version = read_little_endian(stream); + *hashValue = read_little_endian(stream); + size = read_little_endian(stream); + if (!stream || version != Version) return false; + desc->resize(size); + stream.read(&(*desc)[0], size); + return !stream.fail(); + } + + // Write network header + bool write_header(std::ostream& stream, std::uint32_t hashValue, const std::string& desc) + { + write_little_endian(stream, Version); + write_little_endian(stream, hashValue); + write_little_endian(stream, desc.size()); + stream.write(&desc[0], desc.size()); + return !stream.fail(); + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + + std::uint32_t hashValue; + if (!read_header(stream, &hashValue, &netDescription)) return false; + if (hashValue != HashValue) return false; + if (!Detail::read_parameters(stream, *featureTransformer)) return false; + for (std::size_t i = 0; i < LayerStacks; ++i) + if (!Detail::read_parameters(stream, *(network[i]))) return false; + return stream && stream.peek() == std::ios::traits_type::eof(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) { + + if (!write_header(stream, HashValue, netDescription)) return false; + if (!Detail::write_parameters(stream, *featureTransformer)) return false; + for (std::size_t i = 0; i < LayerStacks; ++i) + if (!Detail::write_parameters(stream, *(network[i]))) return false; + return (bool)stream; + } + + // Evaluation function. Perform differential calculation. + Value evaluate(const Position& pos, bool adjusted) { + + // We manually align the arrays on the stack because with gcc < 9.3 + // overaligning stack variables with alignas() doesn't work correctly. + + constexpr uint64_t alignment = CacheLineSize; + int delta = 7; + +#if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) + TransformedFeatureType transformedFeaturesUnaligned[ + FeatureTransformer::BufferSize + alignment / sizeof(TransformedFeatureType)]; + char bufferUnaligned[Network::BufferSize + alignment]; + + auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); + auto* buffer = align_ptr_up(&bufferUnaligned[0]); +#else + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + alignas(alignment) char buffer[Network::BufferSize]; +#endif + + ASSERT_ALIGNED(transformedFeatures, alignment); + ASSERT_ALIGNED(buffer, alignment); + + const std::size_t bucket = (pos.count() - 1) / 4; + const auto psqt = featureTransformer->transform(pos, transformedFeatures, bucket); + const auto positional = network[bucket]->propagate(transformedFeatures, buffer)[0]; + + // Give more value to positional evaluation when adjusted flag is set + if (adjusted) + return static_cast(((128 - delta) * psqt + (128 + delta) * positional) / 128 / OutputScale); + else + return static_cast((psqt + positional) / OutputScale); + } + + struct NnueEvalTrace { + static_assert(LayerStacks == PSQTBuckets); + + Value psqt[LayerStacks]; + Value positional[LayerStacks]; + std::size_t correctBucket; + }; + + static NnueEvalTrace trace_evaluate(const Position& pos) { + + // We manually align the arrays on the stack because with gcc < 9.3 + // overaligning stack variables with alignas() doesn't work correctly. + + constexpr uint64_t alignment = CacheLineSize; + +#if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) + TransformedFeatureType transformedFeaturesUnaligned[ + FeatureTransformer::BufferSize + alignment / sizeof(TransformedFeatureType)]; + char bufferUnaligned[Network::BufferSize + alignment]; + + auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); + auto* buffer = align_ptr_up(&bufferUnaligned[0]); +#else + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + alignas(alignment) char buffer[Network::BufferSize]; +#endif + + ASSERT_ALIGNED(transformedFeatures, alignment); + ASSERT_ALIGNED(buffer, alignment); + + NnueEvalTrace t{}; + t.correctBucket = (pos.count() - 1) / 4; + for (std::size_t bucket = 0; bucket < LayerStacks; ++bucket) { + const auto psqt = featureTransformer->transform(pos, transformedFeatures, bucket); + const auto output = network[bucket]->propagate(transformedFeatures, buffer); + + int materialist = psqt; + int positional = output[0]; + + t.psqt[bucket] = static_cast( materialist / OutputScale ); + t.positional[bucket] = static_cast( positional / OutputScale ); + } + + return t; + } + + static const std::string PieceToChar(" PNBRQK pnbrqk"); + + + // format_cp_compact() converts a Value into (centi)pawns and writes it in a buffer. + // The buffer must have capacity for at least 5 chars. + static void format_cp_compact(Value v, char* buffer) { + + buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' '); + + int cp = std::abs(100 * v / PawnValueEg); + if (cp >= 10000) + { + buffer[1] = '0' + cp / 10000; cp %= 10000; + buffer[2] = '0' + cp / 1000; cp %= 1000; + buffer[3] = '0' + cp / 100; + buffer[4] = ' '; + } + else if (cp >= 1000) + { + buffer[1] = '0' + cp / 1000; cp %= 1000; + buffer[2] = '0' + cp / 100; cp %= 100; + buffer[3] = '.'; + buffer[4] = '0' + cp / 10; + } + else + { + buffer[1] = '0' + cp / 100; cp %= 100; + buffer[2] = '.'; + buffer[3] = '0' + cp / 10; cp %= 10; + buffer[4] = '0' + cp / 1; + } + } + + + // format_cp_aligned_dot() converts a Value into (centi)pawns and writes it in a buffer, + // always keeping two decimals. The buffer must have capacity for at least 7 chars. + static void format_cp_aligned_dot(Value v, char* buffer) { + + buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' '); + + double cp = 1.0 * std::abs(int(v)) / PawnValueEg; + sprintf(&buffer[1], "%6.2f", cp); + } + + + // trace() returns a string with the value of each piece on a board, + // and a table for (PSQT, Layers) values bucket by bucket. + + std::string trace(Position& pos) { + + std::stringstream ss; + + char board[3*8+1][8*8+2]; + std::memset(board, ' ', sizeof(board)); + for (int row = 0; row < 3*8+1; ++row) + board[row][8*8+1] = '\0'; + + // A lambda to output one box of the board + auto writeSquare = [&board](File file, Rank rank, Piece pc, Value value) { + + const int x = ((int)file) * 8; + const int y = (7 - (int)rank) * 3; + for (int i = 1; i < 8; ++i) + board[y][x+i] = board[y+3][x+i] = '-'; + for (int i = 1; i < 3; ++i) + board[y+i][x] = board[y+i][x+8] = '|'; + board[y][x] = board[y][x+8] = board[y+3][x+8] = board[y+3][x] = '+'; + if (pc != NO_PIECE) + board[y+1][x+4] = PieceToChar[pc]; + if (value != VALUE_NONE) + format_cp_compact(value, &board[y+2][x+2]); + }; + + // We estimate the value of each piece by doing a differential evaluation from + // the current base eval, simulating the removal of the piece from its square. + Value base = evaluate(pos); + base = pos.side_to_move() == WHITE ? base : -base; + + for (File f = FILE_A; f <= FILE_H; ++f) + for (Rank r = RANK_1; r <= RANK_8; ++r) + { + Square sq = make_square(f, r); + Piece pc = pos.piece_on(sq); + Value v = VALUE_NONE; + + if (pc != NO_PIECE && type_of(pc) != KING) + { + auto st = pos.state(); + + pos.remove_piece(sq); + st->accumulator.computed[WHITE] = false; + st->accumulator.computed[BLACK] = false; + + Value eval = evaluate(pos); + eval = pos.side_to_move() == WHITE ? eval : -eval; + v = base - eval; + + pos.put_piece(pc, sq); + st->accumulator.computed[WHITE] = false; + st->accumulator.computed[BLACK] = false; + } + + writeSquare(f, r, pc, v); + } + + ss << " NNUE derived piece values:\n"; + for (int row = 0; row < 3*8+1; ++row) + ss << board[row] << '\n'; + ss << '\n'; + + auto t = trace_evaluate(pos); + + ss << " NNUE network contributions " + << (pos.side_to_move() == WHITE ? "(White to move)" : "(Black to move)") << std::endl + << "+------------+------------+------------+------------+\n" + << "| Bucket | Material | Positional | Total |\n" + << "| | (PSQT) | (Layers) | |\n" + << "+------------+------------+------------+------------+\n"; + + for (std::size_t bucket = 0; bucket < LayerStacks; ++bucket) + { + char buffer[3][8]; + std::memset(buffer, '\0', sizeof(buffer)); + + format_cp_aligned_dot(t.psqt[bucket], buffer[0]); + format_cp_aligned_dot(t.positional[bucket], buffer[1]); + format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], buffer[2]); + + ss << "| " << bucket << " " + << " | " << buffer[0] << " " + << " | " << buffer[1] << " " + << " | " << buffer[2] << " " + << " |"; + if (bucket == t.correctBucket) + ss << " <-- this bucket is used"; + ss << '\n'; + } + + ss << "+------------+------------+------------+------------+\n"; + + return ss.str(); + } + + + // Load eval, from a file stream or a memory stream + bool load_eval(std::string name, std::istream& stream) { + + initialize(); + fileName = name; + return read_parameters(stream); + } + + // Save eval, to a file stream or a memory stream + bool save_eval(std::ostream& stream) { + + if (fileName.empty()) + return false; + + return write_parameters(stream); + } + + /// Save eval, to a file given by its name + bool save_eval(const std::optional& filename) { + + std::string actualFilename; + std::string msg; + + if (filename.has_value()) + actualFilename = filename.value(); + else + { + if (currentEvalFileName != EvalFileDefaultName) + { + msg = "Failed to export a net. A non-embedded net can only be saved if the filename is specified"; + + sync_cout << msg << sync_endl; + return false; + } + actualFilename = EvalFileDefaultName; + } + + std::ofstream stream(actualFilename, std::ios_base::binary); + bool saved = save_eval(stream); + + msg = saved ? "Network saved successfully to " + actualFilename + : "Failed to export a net"; + + sync_cout << msg << sync_endl; + return saved; + } + + +} // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/evaluate_nnue.h b/src/nnue/evaluate_nnue.h new file mode 100644 index 00000000000..2e4f1f5098d --- /dev/null +++ b/src/nnue/evaluate_nnue.h @@ -0,0 +1,59 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// header used in NNUE evaluation function + +#ifndef NNUE_EVALUATE_NNUE_H_INCLUDED +#define NNUE_EVALUATE_NNUE_H_INCLUDED + +#include "nnue_feature_transformer.h" + +#include + +namespace Stockfish::Eval::NNUE { + + // Hash value of evaluation function structure + constexpr std::uint32_t HashValue = + FeatureTransformer::get_hash_value() ^ Network::get_hash_value(); + + // Deleter for automating release of memory area + template + struct AlignedDeleter { + void operator()(T* ptr) const { + ptr->~T(); + std_aligned_free(ptr); + } + }; + + template + struct LargePageDeleter { + void operator()(T* ptr) const { + ptr->~T(); + aligned_large_pages_free(ptr); + } + }; + + template + using AlignedPtr = std::unique_ptr>; + + template + using LargePagePtr = std::unique_ptr>; + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_EVALUATE_NNUE_H_INCLUDED diff --git a/src/nnue/features/half_ka_v2_hm.cpp b/src/nnue/features/half_ka_v2_hm.cpp new file mode 100644 index 00000000000..07a1d7a154d --- /dev/null +++ b/src/nnue/features/half_ka_v2_hm.cpp @@ -0,0 +1,83 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//Definition of input features HalfKAv2_hm of NNUE evaluation function + +#include "half_ka_v2_hm.h" + +#include "../../position.h" + +namespace Stockfish::Eval::NNUE::Features { + + // Orient a square according to perspective (rotates by 180 for black) + inline Square HalfKAv2_hm::orient(Color perspective, Square s, Square ksq) { + return Square(int(s) ^ (bool(perspective) * SQ_A8) ^ ((file_of(ksq) < FILE_E) * SQ_H1)); + } + + // Index of a feature for a given king position and another piece on some square + inline IndexType HalfKAv2_hm::make_index(Color perspective, Square s, Piece pc, Square ksq) { + Square o_ksq = orient(perspective, ksq, ksq); + return IndexType(orient(perspective, s, ksq) + PieceSquareIndex[perspective][pc] + PS_NB * KingBuckets[o_ksq]); + } + + // Get a list of indices for active features + void HalfKAv2_hm::append_active_indices( + const Position& pos, + Color perspective, + IndexList& active + ) { + Square ksq = pos.square(perspective); + Bitboard bb = pos.pieces(); + while (bb) + { + Square s = pop_lsb(bb); + active.push_back(make_index(perspective, s, pos.piece_on(s), ksq)); + } + } + + + // append_changed_indices() : get a list of indices for recently changed features + + void HalfKAv2_hm::append_changed_indices( + Square ksq, + const DirtyPiece& dp, + Color perspective, + IndexList& removed, + IndexList& added + ) { + for (int i = 0; i < dp.dirty_num; ++i) { + if (dp.from[i] != SQ_NONE) + removed.push_back(make_index(perspective, dp.from[i], dp.piece[i], ksq)); + if (dp.to[i] != SQ_NONE) + added.push_back(make_index(perspective, dp.to[i], dp.piece[i], ksq)); + } + } + + int HalfKAv2_hm::update_cost(const StateInfo* st) { + return st->dirtyPiece.dirty_num; + } + + int HalfKAv2_hm::refresh_cost(const Position& pos) { + return pos.count(); + } + + bool HalfKAv2_hm::requires_refresh(const StateInfo* st, Color perspective) { + return st->dirtyPiece.piece[0] == make_piece(perspective, KING); + } + +} // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/features/half_ka_v2_hm.h b/src/nnue/features/half_ka_v2_hm.h new file mode 100644 index 00000000000..1e6da0bfe2b --- /dev/null +++ b/src/nnue/features/half_ka_v2_hm.h @@ -0,0 +1,124 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//Definition of input features HalfKP of NNUE evaluation function + +#ifndef NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED +#define NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED + +#include "../nnue_common.h" + +#include "../../evaluate.h" +#include "../../misc.h" + +namespace Stockfish { + struct StateInfo; +} + +namespace Stockfish::Eval::NNUE::Features { + + // Feature HalfKAv2_hm: Combination of the position of own king + // and the position of pieces. Position mirrored such that king always on e..h files. + class HalfKAv2_hm { + + // unique number for each piece type on each square + enum { + PS_NONE = 0, + PS_W_PAWN = 0, + PS_B_PAWN = 1 * SQUARE_NB, + PS_W_KNIGHT = 2 * SQUARE_NB, + PS_B_KNIGHT = 3 * SQUARE_NB, + PS_W_BISHOP = 4 * SQUARE_NB, + PS_B_BISHOP = 5 * SQUARE_NB, + PS_W_ROOK = 6 * SQUARE_NB, + PS_B_ROOK = 7 * SQUARE_NB, + PS_W_QUEEN = 8 * SQUARE_NB, + PS_B_QUEEN = 9 * SQUARE_NB, + PS_KING = 10 * SQUARE_NB, + PS_NB = 11 * SQUARE_NB + }; + + static constexpr IndexType PieceSquareIndex[COLOR_NB][PIECE_NB] = { + // convention: W - us, B - them + // viewed from other side, W and B are reversed + { PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE, + PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE }, + { PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE, + PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE } + }; + + // Orient a square according to perspective (rotates by 180 for black) + static Square orient(Color perspective, Square s, Square ksq); + + // Index of a feature for a given king position and another piece on some square + static IndexType make_index(Color perspective, Square s, Piece pc, Square ksq); + + public: + // Feature name + static constexpr const char* Name = "HalfKAv2_hm(Friend)"; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t HashValue = 0x7f234cb8u; + + // Number of feature dimensions + static constexpr IndexType Dimensions = + static_cast(SQUARE_NB) * static_cast(PS_NB) / 2; + + static constexpr int KingBuckets[64] = { + -1, -1, -1, -1, 31, 30, 29, 28, + -1, -1, -1, -1, 27, 26, 25, 24, + -1, -1, -1, -1, 23, 22, 21, 20, + -1, -1, -1, -1, 19, 18, 17, 16, + -1, -1, -1, -1, 15, 14, 13, 12, + -1, -1, -1, -1, 11, 10, 9, 8, + -1, -1, -1, -1, 7, 6, 5, 4, + -1, -1, -1, -1, 3, 2, 1, 0 + }; + + // Maximum number of simultaneously active features. + static constexpr IndexType MaxActiveDimensions = 32; + using IndexList = ValueList; + + // Get a list of indices for active features + static void append_active_indices( + const Position& pos, + Color perspective, + IndexList& active); + + // Get a list of indices for recently changed features + static void append_changed_indices( + Square ksq, + const DirtyPiece& dp, + Color perspective, + IndexList& removed, + IndexList& added + ); + + // Returns the cost of updating one perspective, the most costly one. + // Assumes no refresh needed. + static int update_cost(const StateInfo* st); + static int refresh_cost(const Position& pos); + + // Returns whether the change stored in this StateInfo means that + // a full accumulator refresh is required. + static bool requires_refresh(const StateInfo* st, Color perspective); + }; + +} // namespace Stockfish::Eval::NNUE::Features + +#endif // #ifndef NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h new file mode 100644 index 00000000000..4e85a5fe4b1 --- /dev/null +++ b/src/nnue/layers/affine_transform.h @@ -0,0 +1,566 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer AffineTransform of NNUE evaluation function + +#ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED +#define NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED + +#include +#include +#include +#include "../nnue_common.h" +#include "../../simd.h" + +/* + This file contains the definition for a fully connected layer (aka affine transform). + Two approaches are employed, depending on the sizes of the transform. + + Approach 1: + - used when the PaddedInputDimensions >= 128 + - uses AVX512 if possible + - processes inputs in batches of 2*InputSimdWidth + - so in batches of 128 for AVX512 + - the weight blocks of size InputSimdWidth are transposed such that + access is sequential + - N columns of the weight matrix are processed a time, where N + depends on the architecture (the amount of registers) + - accumulate + hadd is used + + Approach 2: + - used when the PaddedInputDimensions < 128 + - does not use AVX512 + - expected use-case is for when PaddedInputDimensions == 32 and InputDimensions <= 32. + - that's why AVX512 is hard to implement + - expected use-case is small layers + - not optimized as well as the approach 1 + - inputs are processed in chunks of 4, weights are respectively transposed + - accumulation happens directly to int32s +*/ + +namespace Stockfish::Eval::NNUE::Layers { + +// Fallback implementation for older/other architectures. +// Identical for both approaches. Requires the input to be padded to at least 16 values. +#if !defined(USE_SSSE3) + template + static void affine_transform_non_ssse3(std::int32_t* output, const std::int8_t* weights, const std::int32_t* biases, const std::uint8_t* input) + { +# if defined(USE_SSE2) + // At least a multiple of 16, with SSE2. + static_assert(PaddedInputDimensions % 16 == 0); + constexpr IndexType NumChunks = PaddedInputDimensions / 16; + const __m128i Zeros = _mm_setzero_si128(); + const auto inputVector = reinterpret_cast(input); + +# elif defined(USE_MMX) + static_assert(InputDimensions % 8 == 0); + constexpr IndexType NumChunks = InputDimensions / 8; + const __m64 Zeros = _mm_setzero_si64(); + const auto inputVector = reinterpret_cast(input); + +# elif defined(USE_NEON) + constexpr IndexType NumChunks = (InputDimensions + 15) / 16; + const auto inputVector = reinterpret_cast(input); +# endif + + for (IndexType i = 0; i < OutputDimensions; ++i) { + const IndexType offset = i * PaddedInputDimensions; + +# if defined(USE_SSE2) + __m128i sumLo = _mm_cvtsi32_si128(biases[i]); + __m128i sumHi = Zeros; + const auto row = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < NumChunks; ++j) { + __m128i row_j = _mm_load_si128(&row[j]); + __m128i input_j = _mm_load_si128(&inputVector[j]); + __m128i extendedRowLo = _mm_srai_epi16(_mm_unpacklo_epi8(row_j, row_j), 8); + __m128i extendedRowHi = _mm_srai_epi16(_mm_unpackhi_epi8(row_j, row_j), 8); + __m128i extendedInputLo = _mm_unpacklo_epi8(input_j, Zeros); + __m128i extendedInputHi = _mm_unpackhi_epi8(input_j, Zeros); + __m128i productLo = _mm_madd_epi16(extendedRowLo, extendedInputLo); + __m128i productHi = _mm_madd_epi16(extendedRowHi, extendedInputHi); + sumLo = _mm_add_epi32(sumLo, productLo); + sumHi = _mm_add_epi32(sumHi, productHi); + } + __m128i sum = _mm_add_epi32(sumLo, sumHi); + __m128i sumHigh_64 = _mm_shuffle_epi32(sum, _MM_SHUFFLE(1, 0, 3, 2)); + sum = _mm_add_epi32(sum, sumHigh_64); + __m128i sum_second_32 = _mm_shufflelo_epi16(sum, _MM_SHUFFLE(1, 0, 3, 2)); + sum = _mm_add_epi32(sum, sum_second_32); + output[i] = _mm_cvtsi128_si32(sum); + +# elif defined(USE_MMX) + __m64 sumLo = _mm_cvtsi32_si64(biases[i]); + __m64 sumHi = Zeros; + const auto row = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < NumChunks; ++j) { + __m64 row_j = row[j]; + __m64 input_j = inputVector[j]; + __m64 extendedRowLo = _mm_srai_pi16(_mm_unpacklo_pi8(row_j, row_j), 8); + __m64 extendedRowHi = _mm_srai_pi16(_mm_unpackhi_pi8(row_j, row_j), 8); + __m64 extendedInputLo = _mm_unpacklo_pi8(input_j, Zeros); + __m64 extendedInputHi = _mm_unpackhi_pi8(input_j, Zeros); + __m64 productLo = _mm_madd_pi16(extendedRowLo, extendedInputLo); + __m64 productHi = _mm_madd_pi16(extendedRowHi, extendedInputHi); + sumLo = _mm_add_pi32(sumLo, productLo); + sumHi = _mm_add_pi32(sumHi, productHi); + } + __m64 sum = _mm_add_pi32(sumLo, sumHi); + sum = _mm_add_pi32(sum, _mm_unpackhi_pi32(sum, sum)); + output[i] = _mm_cvtsi64_si32(sum); + +# elif defined(USE_NEON) + int32x4_t sum = {biases[i]}; + const auto row = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < NumChunks; ++j) { + int16x8_t product = vmull_s8(inputVector[j * 2], row[j * 2]); + product = vmlal_s8(product, inputVector[j * 2 + 1], row[j * 2 + 1]); + sum = vpadalq_s16(sum, product); + } + output[i] = sum[0] + sum[1] + sum[2] + sum[3]; + +# else + std::int32_t sum = biases[i]; + for (IndexType j = 0; j < InputDimensions; ++j) { + sum += weights[offset + j] * input[j]; + } + output[i] = sum; +# endif + } + +# if defined(USE_MMX) + _mm_empty(); +# endif + } +#endif + + template + class AffineTransform; + + // A specialization for large inputs. + template + class AffineTransform= 2*64-1)>> { + public: + // Input/output type + using InputType = typename PreviousLayer::OutputType; + using OutputType = std::int32_t; + static_assert(std::is_same::value, ""); + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = PreviousLayer::OutputDimensions; + static constexpr IndexType OutputDimensions = OutDims; + + static constexpr IndexType PaddedInputDimensions = + ceil_to_multiple(InputDimensions, MaxSimdWidth); + + static_assert(PaddedInputDimensions >= 128, "Something went wrong. This specialization should not have been chosen."); + +#if defined (USE_AVX512) + static constexpr const IndexType InputSimdWidth = 64; + static constexpr const IndexType MaxNumOutputRegs = 16; +#elif defined (USE_AVX2) + static constexpr const IndexType InputSimdWidth = 32; + static constexpr const IndexType MaxNumOutputRegs = 8; +#elif defined (USE_SSSE3) + static constexpr const IndexType InputSimdWidth = 16; + static constexpr const IndexType MaxNumOutputRegs = 8; +#elif defined (USE_NEON) + static constexpr const IndexType InputSimdWidth = 8; + static constexpr const IndexType MaxNumOutputRegs = 8; +#else + // The fallback implementation will not have permuted weights. + // We define these to avoid a lot of ifdefs later. + static constexpr const IndexType InputSimdWidth = 1; + static constexpr const IndexType MaxNumOutputRegs = 1; +#endif + + // A big block is a region in the weight matrix of the size [PaddedInputDimensions, NumOutputRegs]. + // A small block is a region of size [InputSimdWidth, 1] + + static constexpr const IndexType NumOutputRegs = std::min(MaxNumOutputRegs, OutputDimensions); + static constexpr const IndexType SmallBlockSize = InputSimdWidth; + static constexpr const IndexType BigBlockSize = NumOutputRegs * PaddedInputDimensions; + static constexpr const IndexType NumSmallBlocksInBigBlock = BigBlockSize / SmallBlockSize; + static constexpr const IndexType NumSmallBlocksPerOutput = PaddedInputDimensions / SmallBlockSize; + static constexpr const IndexType NumBigBlocks = OutputDimensions / NumOutputRegs; + + static_assert(OutputDimensions % NumOutputRegs == 0); + + // Size of forward propagation buffer used in this layer + static constexpr std::size_t SelfBufferSize = + ceil_to_multiple(OutputDimensions * sizeof(OutputType), CacheLineSize); + + // Size of the forward propagation buffer used from the input layer to this layer + static constexpr std::size_t BufferSize = + PreviousLayer::BufferSize + SelfBufferSize; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + std::uint32_t hashValue = 0xCC03DAE4u; + hashValue += OutputDimensions; + hashValue ^= PreviousLayer::get_hash_value() >> 1; + hashValue ^= PreviousLayer::get_hash_value() << 31; + return hashValue; + } + + /* + Transposes the small blocks within a block. + Effectively means that weights can be traversed sequentially during inference. + */ + static IndexType get_weight_index(IndexType i) + { + const IndexType smallBlock = (i / SmallBlockSize) % NumSmallBlocksInBigBlock; + const IndexType smallBlockCol = smallBlock / NumSmallBlocksPerOutput; + const IndexType smallBlockRow = smallBlock % NumSmallBlocksPerOutput; + const IndexType bigBlock = i / BigBlockSize; + const IndexType rest = i % SmallBlockSize; + + const IndexType idx = + bigBlock * BigBlockSize + + smallBlockRow * SmallBlockSize * NumOutputRegs + + smallBlockCol * SmallBlockSize + + rest; + + return idx; + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + if (!previousLayer.read_parameters(stream)) return false; + for (std::size_t i = 0; i < OutputDimensions; ++i) + biases[i] = read_little_endian(stream); + + for (std::size_t i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + weights[get_weight_index(i)] = read_little_endian(stream); + + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + if (!previousLayer.write_parameters(stream)) return false; + for (std::size_t i = 0; i < OutputDimensions; ++i) + write_little_endian(stream, biases[i]); + + for (std::size_t i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + write_little_endian(stream, weights[get_weight_index(i)]); + + return !stream.fail(); + } + + // Forward propagation + const OutputType* propagate( + const TransformedFeatureType* transformedFeatures, char* buffer) const { + const auto input = previousLayer.propagate( + transformedFeatures, buffer + SelfBufferSize); + OutputType* output = reinterpret_cast(buffer); + +#if defined (USE_AVX512) + using acc_vec_t = __m512i; + using bias_vec_t = __m128i; + using weight_vec_t = __m512i; + using in_vec_t = __m512i; + #define vec_zero _mm512_setzero_si512() + #define vec_add_dpbusd_32x2 Simd::m512_add_dpbusd_epi32x2 + #define vec_hadd Simd::m512_hadd + #define vec_haddx4 Simd::m512_haddx4 +#elif defined (USE_AVX2) + using acc_vec_t = __m256i; + using bias_vec_t = __m128i; + using weight_vec_t = __m256i; + using in_vec_t = __m256i; + #define vec_zero _mm256_setzero_si256() + #define vec_add_dpbusd_32x2 Simd::m256_add_dpbusd_epi32x2 + #define vec_hadd Simd::m256_hadd + #define vec_haddx4 Simd::m256_haddx4 +#elif defined (USE_SSSE3) + using acc_vec_t = __m128i; + using bias_vec_t = __m128i; + using weight_vec_t = __m128i; + using in_vec_t = __m128i; + #define vec_zero _mm_setzero_si128() + #define vec_add_dpbusd_32x2 Simd::m128_add_dpbusd_epi32x2 + #define vec_hadd Simd::m128_hadd + #define vec_haddx4 Simd::m128_haddx4 +#elif defined (USE_NEON) + using acc_vec_t = int32x4_t; + using bias_vec_t = int32x4_t; + using weight_vec_t = int8x8_t; + using in_vec_t = int8x8_t; + #define vec_zero {0} + #define vec_add_dpbusd_32x2 Simd::neon_m128_add_dpbusd_epi32x2 + #define vec_hadd Simd::neon_m128_hadd + #define vec_haddx4 Simd::neon_m128_haddx4 +#endif + +#if defined (USE_SSSE3) || defined (USE_NEON) + const in_vec_t* invec = reinterpret_cast(input); + + + // Perform accumulation to registers for each big block + for (IndexType bigBlock = 0; bigBlock < NumBigBlocks; ++bigBlock) + { + acc_vec_t acc[NumOutputRegs] = { vec_zero }; + + // Each big block has NumOutputRegs small blocks in each "row", one per register. + // We process two small blocks at a time to save on one addition without VNNI. + for (IndexType smallBlock = 0; smallBlock < NumSmallBlocksPerOutput; smallBlock += 2) + { + const weight_vec_t* weightvec = + reinterpret_cast( + weights + + bigBlock * BigBlockSize + + smallBlock * SmallBlockSize * NumOutputRegs); + + const in_vec_t in0 = invec[smallBlock + 0]; + const in_vec_t in1 = invec[smallBlock + 1]; + + for (IndexType k = 0; k < NumOutputRegs; ++k) + vec_add_dpbusd_32x2(acc[k], in0, weightvec[k], in1, weightvec[k + NumOutputRegs]); + } + + // Horizontally add all accumulators. + if constexpr (NumOutputRegs % 4 == 0) + { + bias_vec_t* outputvec = reinterpret_cast(output); + const bias_vec_t* biasvec = reinterpret_cast(biases); + + for (IndexType k = 0; k < NumOutputRegs; k += 4) + { + const IndexType idx = (bigBlock * NumOutputRegs + k) / 4; + outputvec[idx] = vec_haddx4(acc[k+0], acc[k+1], acc[k+2], acc[k+3], biasvec[idx]); + } + } + else + { + for (IndexType k = 0; k < NumOutputRegs; ++k) + { + const IndexType idx = (bigBlock * NumOutputRegs + k); + output[idx] = vec_hadd(acc[k], biases[idx]); + } + } + } + +# undef vec_zero +# undef vec_add_dpbusd_32x2 +# undef vec_hadd +# undef vec_haddx4 +#else + // Use old implementation for the other architectures. + affine_transform_non_ssse3< + InputDimensions, + PaddedInputDimensions, + OutputDimensions>(output, weights, biases, input); + +#endif + + return output; + } + + private: + using BiasType = OutputType; + using WeightType = std::int8_t; + + PreviousLayer previousLayer; + + alignas(CacheLineSize) BiasType biases[OutputDimensions]; + alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions]; + }; + + template + class AffineTransform> { + public: + // Input/output type + using InputType = typename PreviousLayer::OutputType; + using OutputType = std::int32_t; + static_assert(std::is_same::value, ""); + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = + PreviousLayer::OutputDimensions; + static constexpr IndexType OutputDimensions = OutDims; + static constexpr IndexType PaddedInputDimensions = + ceil_to_multiple(InputDimensions, MaxSimdWidth); + + static_assert(PaddedInputDimensions < 128, "Something went wrong. This specialization should not have been chosen."); + +#if defined (USE_SSSE3) + static constexpr const IndexType OutputSimdWidth = SimdWidth / 4; + static constexpr const IndexType InputSimdWidth = SimdWidth; +#endif + + // Size of forward propagation buffer used in this layer + static constexpr std::size_t SelfBufferSize = + ceil_to_multiple(OutputDimensions * sizeof(OutputType), CacheLineSize); + + // Size of the forward propagation buffer used from the input layer to this layer + static constexpr std::size_t BufferSize = + PreviousLayer::BufferSize + SelfBufferSize; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + std::uint32_t hashValue = 0xCC03DAE4u; + hashValue += OutputDimensions; + hashValue ^= PreviousLayer::get_hash_value() >> 1; + hashValue ^= PreviousLayer::get_hash_value() << 31; + return hashValue; + } + + static IndexType get_weight_index_scrambled(IndexType i) + { + return + (i / 4) % (PaddedInputDimensions / 4) * OutputDimensions * 4 + + i / PaddedInputDimensions * 4 + + i % 4; + } + + static IndexType get_weight_index(IndexType i) + { +#if defined (USE_SSSE3) + return get_weight_index_scrambled(i); +#else + return i; +#endif + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + if (!previousLayer.read_parameters(stream)) return false; + for (std::size_t i = 0; i < OutputDimensions; ++i) + biases[i] = read_little_endian(stream); + for (std::size_t i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + weights[get_weight_index(i)] = read_little_endian(stream); + + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + if (!previousLayer.write_parameters(stream)) return false; + for (std::size_t i = 0; i < OutputDimensions; ++i) + write_little_endian(stream, biases[i]); + + for (std::size_t i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + write_little_endian(stream, weights[get_weight_index(i)]); + + return !stream.fail(); + } + // Forward propagation + const OutputType* propagate( + const TransformedFeatureType* transformedFeatures, char* buffer) const { + const auto input = previousLayer.propagate( + transformedFeatures, buffer + SelfBufferSize); + const auto output = reinterpret_cast(buffer); + +#if defined (USE_AVX2) + using vec_t = __m256i; + #define vec_setzero _mm256_setzero_si256 + #define vec_set_32 _mm256_set1_epi32 + #define vec_add_dpbusd_32 Simd::m256_add_dpbusd_epi32 + #define vec_add_dpbusd_32x2 Simd::m256_add_dpbusd_epi32x2 + #define vec_add_dpbusd_32x4 Simd::m256_add_dpbusd_epi32x4 + #define vec_hadd Simd::m256_hadd + #define vec_haddx4 Simd::m256_haddx4 +#elif defined (USE_SSSE3) + using vec_t = __m128i; + #define vec_setzero _mm_setzero_si128 + #define vec_set_32 _mm_set1_epi32 + #define vec_add_dpbusd_32 Simd::m128_add_dpbusd_epi32 + #define vec_add_dpbusd_32x2 Simd::m128_add_dpbusd_epi32x2 + #define vec_add_dpbusd_32x4 Simd::m128_add_dpbusd_epi32x4 + #define vec_hadd Simd::m128_hadd + #define vec_haddx4 Simd::m128_haddx4 +#endif + +#if defined (USE_SSSE3) + const auto inputVector = reinterpret_cast(input); + + static_assert(InputDimensions % 8 == 0); + static_assert(OutputDimensions % OutputSimdWidth == 0 || OutputDimensions == 1); + + if constexpr (OutputDimensions % OutputSimdWidth == 0) + { + constexpr IndexType NumChunks = InputDimensions / 4; + constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth; + + const auto input32 = reinterpret_cast(input); + const vec_t* biasvec = reinterpret_cast(biases); + vec_t acc[NumRegs]; + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = biasvec[k]; + + for (IndexType i = 0; i < NumChunks; i += 2) + { + const vec_t in0 = vec_set_32(input32[i + 0]); + const vec_t in1 = vec_set_32(input32[i + 1]); + const auto col0 = reinterpret_cast(&weights[(i + 0) * OutputDimensions * 4]); + const auto col1 = reinterpret_cast(&weights[(i + 1) * OutputDimensions * 4]); + for (IndexType k = 0; k < NumRegs; ++k) + vec_add_dpbusd_32x2(acc[k], in0, col0[k], in1, col1[k]); + } + + vec_t* outptr = reinterpret_cast(output); + for (IndexType k = 0; k < NumRegs; ++k) + outptr[k] = acc[k]; + } + else if constexpr (OutputDimensions == 1) + { + constexpr IndexType NumChunks = PaddedInputDimensions / SimdWidth; + vec_t sum0 = vec_setzero(); + const auto row0 = reinterpret_cast(&weights[0]); + + for (int j = 0; j < (int)NumChunks; ++j) + { + const vec_t in = inputVector[j]; + vec_add_dpbusd_32(sum0, in, row0[j]); + } + output[0] = vec_hadd(sum0, biases[0]); + } + +# undef vec_setzero +# undef vec_set_32 +# undef vec_add_dpbusd_32 +# undef vec_add_dpbusd_32x2 +# undef vec_add_dpbusd_32x4 +# undef vec_hadd +# undef vec_haddx4 +#else + // Use old implementation for the other architectures. + affine_transform_non_ssse3< + InputDimensions, + PaddedInputDimensions, + OutputDimensions>(output, weights, biases, input); +#endif + + return output; + } + + private: + using BiasType = OutputType; + using WeightType = std::int8_t; + + PreviousLayer previousLayer; + + alignas(CacheLineSize) BiasType biases[OutputDimensions]; + alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions]; + }; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // #ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED diff --git a/src/nnue/layers/clipped_relu.h b/src/nnue/layers/clipped_relu.h new file mode 100644 index 00000000000..0da5e821011 --- /dev/null +++ b/src/nnue/layers/clipped_relu.h @@ -0,0 +1,201 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer ClippedReLU of NNUE evaluation function + +#ifndef NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED +#define NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED + +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Layers { + + // Clipped ReLU + template + class ClippedReLU { + public: + // Input/output type + using InputType = typename PreviousLayer::OutputType; + using OutputType = std::uint8_t; + static_assert(std::is_same::value, ""); + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = PreviousLayer::OutputDimensions; + static constexpr IndexType OutputDimensions = InputDimensions; + static constexpr IndexType PaddedOutputDimensions = + ceil_to_multiple(OutputDimensions, 32); + + // Size of forward propagation buffer used in this layer + static constexpr std::size_t SelfBufferSize = + ceil_to_multiple(OutputDimensions * sizeof(OutputType), CacheLineSize); + + // Size of the forward propagation buffer used from the input layer to this layer + static constexpr std::size_t BufferSize = + PreviousLayer::BufferSize + SelfBufferSize; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + std::uint32_t hashValue = 0x538D24C7u; + hashValue += PreviousLayer::get_hash_value(); + return hashValue; + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + return previousLayer.read_parameters(stream); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + return previousLayer.write_parameters(stream); + } + + // Forward propagation + const OutputType* propagate( + const TransformedFeatureType* transformedFeatures, char* buffer) const { + const auto input = previousLayer.propagate( + transformedFeatures, buffer + SelfBufferSize); + const auto output = reinterpret_cast(buffer); + + #if defined(USE_AVX2) + if constexpr (InputDimensions % SimdWidth == 0) { + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + const __m256i Zero = _mm256_setzero_si256(); + const __m256i Offsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m256i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) { + const __m256i words0 = _mm256_srai_epi16(_mm256_packs_epi32( + _mm256_load_si256(&in[i * 4 + 0]), + _mm256_load_si256(&in[i * 4 + 1])), WeightScaleBits); + const __m256i words1 = _mm256_srai_epi16(_mm256_packs_epi32( + _mm256_load_si256(&in[i * 4 + 2]), + _mm256_load_si256(&in[i * 4 + 3])), WeightScaleBits); + _mm256_store_si256(&out[i], _mm256_permutevar8x32_epi32(_mm256_max_epi8( + _mm256_packs_epi16(words0, words1), Zero), Offsets)); + } + } else { + constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2); + const __m128i Zero = _mm_setzero_si128(); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) { + const __m128i words0 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 0]), + _mm_load_si128(&in[i * 4 + 1])), WeightScaleBits); + const __m128i words1 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 2]), + _mm_load_si128(&in[i * 4 + 3])), WeightScaleBits); + const __m128i packedbytes = _mm_packs_epi16(words0, words1); + _mm_store_si128(&out[i], _mm_max_epi8(packedbytes, Zero)); + } + } + constexpr IndexType Start = + InputDimensions % SimdWidth == 0 + ? InputDimensions / SimdWidth * SimdWidth + : InputDimensions / (SimdWidth / 2) * (SimdWidth / 2); + + #elif defined(USE_SSE2) + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + + #ifdef USE_SSE41 + const __m128i Zero = _mm_setzero_si128(); + #else + const __m128i k0x80s = _mm_set1_epi8(-128); + #endif + + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) { + const __m128i words0 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 0]), + _mm_load_si128(&in[i * 4 + 1])), WeightScaleBits); + const __m128i words1 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 2]), + _mm_load_si128(&in[i * 4 + 3])), WeightScaleBits); + const __m128i packedbytes = _mm_packs_epi16(words0, words1); + _mm_store_si128(&out[i], + + #ifdef USE_SSE41 + _mm_max_epi8(packedbytes, Zero) + #else + _mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s) + #endif + + ); + } + constexpr IndexType Start = NumChunks * SimdWidth; + + #elif defined(USE_MMX) + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + const __m64 k0x80s = _mm_set1_pi8(-128); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m64*>(output); + for (IndexType i = 0; i < NumChunks; ++i) { + const __m64 words0 = _mm_srai_pi16( + _mm_packs_pi32(in[i * 4 + 0], in[i * 4 + 1]), + WeightScaleBits); + const __m64 words1 = _mm_srai_pi16( + _mm_packs_pi32(in[i * 4 + 2], in[i * 4 + 3]), + WeightScaleBits); + const __m64 packedbytes = _mm_packs_pi16(words0, words1); + out[i] = _mm_subs_pi8(_mm_adds_pi8(packedbytes, k0x80s), k0x80s); + } + _mm_empty(); + constexpr IndexType Start = NumChunks * SimdWidth; + + #elif defined(USE_NEON) + constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2); + const int8x8_t Zero = {0}; + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast(output); + for (IndexType i = 0; i < NumChunks; ++i) { + int16x8_t shifted; + const auto pack = reinterpret_cast(&shifted); + pack[0] = vqshrn_n_s32(in[i * 2 + 0], WeightScaleBits); + pack[1] = vqshrn_n_s32(in[i * 2 + 1], WeightScaleBits); + out[i] = vmax_s8(vqmovn_s16(shifted), Zero); + } + constexpr IndexType Start = NumChunks * (SimdWidth / 2); + #else + constexpr IndexType Start = 0; + #endif + + for (IndexType i = Start; i < InputDimensions; ++i) { + output[i] = static_cast( + std::max(0, std::min(127, input[i] >> WeightScaleBits))); + } + + // Affine transform layers expect that there is at least + // ceil_to_multiple(OutputDimensions, 32) initialized values. + // We cannot do this in the affine transform because it requires + // preallocating space here. + for (IndexType i = OutputDimensions; i < PaddedOutputDimensions; ++i) { + output[i] = 0; + } + + return output; + } + + private: + PreviousLayer previousLayer; + }; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED diff --git a/src/nnue/layers/input_slice.h b/src/nnue/layers/input_slice.h new file mode 100644 index 00000000000..8f526b745f7 --- /dev/null +++ b/src/nnue/layers/input_slice.h @@ -0,0 +1,73 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// NNUE evaluation function layer InputSlice definition + +#ifndef NNUE_LAYERS_INPUT_SLICE_H_INCLUDED +#define NNUE_LAYERS_INPUT_SLICE_H_INCLUDED + +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Layers { + +// Input layer +template +class InputSlice { + public: + // Need to maintain alignment + static_assert(Offset % MaxSimdWidth == 0, ""); + + // Output type + using OutputType = TransformedFeatureType; + + // Output dimensionality + static constexpr IndexType OutputDimensions = OutDims; + + // Size of forward propagation buffer used from the input layer to this layer + static constexpr std::size_t BufferSize = 0; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + std::uint32_t hashValue = 0xEC42E90Du; + hashValue ^= OutputDimensions ^ (Offset << 10); + return hashValue; + } + + // Read network parameters + bool read_parameters(std::istream& /*stream*/) { + return true; + } + + // Write network parameters + bool write_parameters(std::ostream& /*stream*/) const { + return true; + } + + // Forward propagation + const OutputType* propagate( + const TransformedFeatureType* transformedFeatures, + char* /*buffer*/) const { + return transformedFeatures + Offset; + } + + private: +}; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // #ifndef NNUE_LAYERS_INPUT_SLICE_H_INCLUDED diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h new file mode 100644 index 00000000000..600483b5cfd --- /dev/null +++ b/src/nnue/nnue_accumulator.h @@ -0,0 +1,37 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Class for difference calculation of NNUE evaluation function + +#ifndef NNUE_ACCUMULATOR_H_INCLUDED +#define NNUE_ACCUMULATOR_H_INCLUDED + +#include "nnue_architecture.h" + +namespace Stockfish::Eval::NNUE { + + // Class that holds the result of affine transformation of input features + struct alignas(CacheLineSize) Accumulator { + std::int16_t accumulation[2][TransformedFeatureDimensions]; + std::int32_t psqtAccumulation[2][PSQTBuckets]; + bool computed[2]; + }; + +} // namespace Stockfish::Eval::NNUE + +#endif // NNUE_ACCUMULATOR_H_INCLUDED diff --git a/src/nnue/nnue_architecture.h b/src/nnue/nnue_architecture.h new file mode 100644 index 00000000000..8867fac72fc --- /dev/null +++ b/src/nnue/nnue_architecture.h @@ -0,0 +1,60 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Input features and network structure used in NNUE evaluation function + +#ifndef NNUE_ARCHITECTURE_H_INCLUDED +#define NNUE_ARCHITECTURE_H_INCLUDED + +#include "nnue_common.h" + +#include "features/half_ka_v2_hm.h" + +#include "layers/input_slice.h" +#include "layers/affine_transform.h" +#include "layers/clipped_relu.h" + +namespace Stockfish::Eval::NNUE { + + // Input features used in evaluation function + using FeatureSet = Features::HalfKAv2_hm; + + // Number of input feature dimensions after conversion + constexpr IndexType TransformedFeatureDimensions = 1024; + constexpr IndexType PSQTBuckets = 8; + constexpr IndexType LayerStacks = 8; + + namespace Layers { + + // Define network structure + using InputLayer = InputSlice; + using HiddenLayer1 = ClippedReLU>; + using HiddenLayer2 = ClippedReLU>; + using OutputLayer = AffineTransform; + + } // namespace Layers + + using Network = Layers::OutputLayer; + + static_assert(TransformedFeatureDimensions % MaxSimdWidth == 0, ""); + static_assert(Network::OutputDimensions == 1, ""); + static_assert(std::is_same::value, ""); + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_ARCHITECTURE_H_INCLUDED diff --git a/src/nnue/nnue_common.h b/src/nnue/nnue_common.h new file mode 100644 index 00000000000..1bce00ae465 --- /dev/null +++ b/src/nnue/nnue_common.h @@ -0,0 +1,164 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Constants used in NNUE evaluation function + +#ifndef NNUE_COMMON_H_INCLUDED +#define NNUE_COMMON_H_INCLUDED + +#include +#include + +#include "../misc.h" // for IsLittleEndian + +#if defined(USE_AVX2) +#include + +#elif defined(USE_SSE41) +#include + +#elif defined(USE_SSSE3) +#include + +#elif defined(USE_SSE2) +#include + +#elif defined(USE_MMX) +#include + +#elif defined(USE_NEON) +#include +#endif + +namespace Stockfish::Eval::NNUE { + + // Version of the evaluation file + constexpr std::uint32_t Version = 0x7AF32F20u; + + // Constant used in evaluation value calculation + constexpr int OutputScale = 16; + constexpr int WeightScaleBits = 6; + + // Size of cache line (in bytes) + constexpr std::size_t CacheLineSize = 64; + + // SIMD width (in bytes) + #if defined(USE_AVX2) + constexpr std::size_t SimdWidth = 32; + + #elif defined(USE_SSE2) + constexpr std::size_t SimdWidth = 16; + + #elif defined(USE_MMX) + constexpr std::size_t SimdWidth = 8; + + #elif defined(USE_NEON) + constexpr std::size_t SimdWidth = 16; + #endif + + constexpr std::size_t MaxSimdWidth = 32; + + // Type of input feature after conversion + using TransformedFeatureType = std::uint8_t; + using IndexType = std::uint32_t; + + // Round n up to be a multiple of base + template + constexpr IntType ceil_to_multiple(IntType n, IntType base) { + return (n + base - 1) / base * base; + } + + // read_little_endian() is our utility to read an integer (signed or unsigned, any size) + // from a stream in little-endian order. We swap the byte order after the read if + // necessary to return a result with the byte ordering of the compiling machine. + template + inline IntType read_little_endian(std::istream& stream) { + IntType result; + + if (IsLittleEndian) + stream.read(reinterpret_cast(&result), sizeof(IntType)); + else + { + std::uint8_t u[sizeof(IntType)]; + typename std::make_unsigned::type v = 0; + + stream.read(reinterpret_cast(u), sizeof(IntType)); + for (std::size_t i = 0; i < sizeof(IntType); ++i) + v = (v << 8) | u[sizeof(IntType) - i - 1]; + + std::memcpy(&result, &v, sizeof(IntType)); + } + + return result; + } + + // write_little_endian() is our utility to write an integer (signed or unsigned, any size) + // to a stream in little-endian order. We swap the byte order before the write if + // necessary to always write in little endian order, independently of the byte + // ordering of the compiling machine. + template + inline void write_little_endian(std::ostream& stream, IntType value) { + + if (IsLittleEndian) + stream.write(reinterpret_cast(&value), sizeof(IntType)); + else + { + std::uint8_t u[sizeof(IntType)]; + typename std::make_unsigned::type v = value; + + std::size_t i = 0; + // if constexpr to silence the warning about shift by 8 + if constexpr (sizeof(IntType) > 1) + { + for (; i + 1 < sizeof(IntType); ++i) + { + u[i] = v; + v >>= 8; + } + } + u[i] = v; + + stream.write(reinterpret_cast(u), sizeof(IntType)); + } + } + + // read_little_endian(s, out, N) : read integers in bulk from a little indian stream. + // This reads N integers from stream s and put them in array out. + template + inline void read_little_endian(std::istream& stream, IntType* out, std::size_t count) { + if (IsLittleEndian) + stream.read(reinterpret_cast(out), sizeof(IntType) * count); + else + for (std::size_t i = 0; i < count; ++i) + out[i] = read_little_endian(stream); + } + + // write_little_endian(s, values, N) : write integers in bulk to a little indian stream. + // This takes N integers from array values and writes them on stream s. + template + inline void write_little_endian(std::ostream& stream, const IntType* values, std::size_t count) { + if (IsLittleEndian) + stream.write(reinterpret_cast(values), sizeof(IntType) * count); + else + for (std::size_t i = 0; i < count; ++i) + write_little_endian(stream, values[i]); + } + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_COMMON_H_INCLUDED diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h new file mode 100644 index 00000000000..f4024dce83b --- /dev/null +++ b/src/nnue/nnue_feature_transformer.h @@ -0,0 +1,621 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// A class that converts the input features of the NNUE evaluation function + +#ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED +#define NNUE_FEATURE_TRANSFORMER_H_INCLUDED + +#include "nnue_common.h" +#include "nnue_architecture.h" + +#include // std::memset() + +namespace Stockfish::Eval::NNUE { + + using BiasType = std::int16_t; + using WeightType = std::int16_t; + using PSQTWeightType = std::int32_t; + + // If vector instructions are enabled, we update and refresh the + // accumulator tile by tile such that each tile fits in the CPU's + // vector registers. + #define VECTOR + + static_assert(PSQTBuckets % 8 == 0, + "Per feature PSQT values cannot be processed at granularity lower than 8 at a time."); + + #ifdef USE_AVX512 + typedef __m512i vec_t; + typedef __m256i psqt_vec_t; + #define vec_load(a) _mm512_load_si512(a) + #define vec_store(a,b) _mm512_store_si512(a,b) + #define vec_add_16(a,b) _mm512_add_epi16(a,b) + #define vec_sub_16(a,b) _mm512_sub_epi16(a,b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a,b) _mm256_store_si256(a,b) + #define vec_add_psqt_32(a,b) _mm256_add_epi32(a,b) + #define vec_sub_psqt_32(a,b) _mm256_sub_epi32(a,b) + #define vec_zero_psqt() _mm256_setzero_si256() + #define NumRegistersSIMD 32 + + #elif USE_AVX2 + typedef __m256i vec_t; + typedef __m256i psqt_vec_t; + #define vec_load(a) _mm256_load_si256(a) + #define vec_store(a,b) _mm256_store_si256(a,b) + #define vec_add_16(a,b) _mm256_add_epi16(a,b) + #define vec_sub_16(a,b) _mm256_sub_epi16(a,b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a,b) _mm256_store_si256(a,b) + #define vec_add_psqt_32(a,b) _mm256_add_epi32(a,b) + #define vec_sub_psqt_32(a,b) _mm256_sub_epi32(a,b) + #define vec_zero_psqt() _mm256_setzero_si256() + #define NumRegistersSIMD 16 + + #elif USE_SSE2 + typedef __m128i vec_t; + typedef __m128i psqt_vec_t; + #define vec_load(a) (*(a)) + #define vec_store(a,b) *(a)=(b) + #define vec_add_16(a,b) _mm_add_epi16(a,b) + #define vec_sub_16(a,b) _mm_sub_epi16(a,b) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a,b) *(a)=(b) + #define vec_add_psqt_32(a,b) _mm_add_epi32(a,b) + #define vec_sub_psqt_32(a,b) _mm_sub_epi32(a,b) + #define vec_zero_psqt() _mm_setzero_si128() + #define NumRegistersSIMD (Is64Bit ? 16 : 8) + + #elif USE_MMX + typedef __m64 vec_t; + typedef __m64 psqt_vec_t; + #define vec_load(a) (*(a)) + #define vec_store(a,b) *(a)=(b) + #define vec_add_16(a,b) _mm_add_pi16(a,b) + #define vec_sub_16(a,b) _mm_sub_pi16(a,b) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a,b) *(a)=(b) + #define vec_add_psqt_32(a,b) _mm_add_pi32(a,b) + #define vec_sub_psqt_32(a,b) _mm_sub_pi32(a,b) + #define vec_zero_psqt() _mm_setzero_si64() + #define NumRegistersSIMD 8 + + #elif USE_NEON + typedef int16x8_t vec_t; + typedef int32x4_t psqt_vec_t; + #define vec_load(a) (*(a)) + #define vec_store(a,b) *(a)=(b) + #define vec_add_16(a,b) vaddq_s16(a,b) + #define vec_sub_16(a,b) vsubq_s16(a,b) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a,b) *(a)=(b) + #define vec_add_psqt_32(a,b) vaddq_s32(a,b) + #define vec_sub_psqt_32(a,b) vsubq_s32(a,b) + #define vec_zero_psqt() psqt_vec_t{0} + #define NumRegistersSIMD 16 + + #else + #undef VECTOR + + #endif + + + #ifdef VECTOR + + // Compute optimal SIMD register count for feature transformer accumulation. + + // We use __m* types as template arguments, which causes GCC to emit warnings + // about losing some attribute information. This is irrelevant to us as we + // only take their size, so the following pragma are harmless. + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wignored-attributes" + + template + static constexpr int BestRegisterCount() + { + #define RegisterSize sizeof(SIMDRegisterType) + #define LaneSize sizeof(LaneType) + + static_assert(RegisterSize >= LaneSize); + static_assert(MaxRegisters <= NumRegistersSIMD); + static_assert(MaxRegisters > 0); + static_assert(NumRegistersSIMD > 0); + static_assert(RegisterSize % LaneSize == 0); + static_assert((NumLanes * LaneSize) % RegisterSize == 0); + + const int ideal = (NumLanes * LaneSize) / RegisterSize; + if (ideal <= MaxRegisters) + return ideal; + + // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters + for (int divisor = MaxRegisters; divisor > 1; --divisor) + if (ideal % divisor == 0) + return divisor; + + return 1; + } + + static constexpr int NumRegs = BestRegisterCount(); + static constexpr int NumPsqtRegs = BestRegisterCount(); + + #pragma GCC diagnostic pop + + #endif + + + + // Input feature converter + class FeatureTransformer { + + private: + // Number of output dimensions for one side + static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; + + #ifdef VECTOR + static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; + static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; + static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); + static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); + #endif + + public: + // Output type + using OutputType = TransformedFeatureType; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = FeatureSet::Dimensions; + static constexpr IndexType OutputDimensions = HalfDimensions * 2; + + // Size of forward propagation buffer + static constexpr std::size_t BufferSize = + OutputDimensions * sizeof(OutputType); + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + return FeatureSet::HashValue ^ OutputDimensions; + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + + read_little_endian(stream, biases , HalfDimensions ); + read_little_endian(stream, weights , HalfDimensions * InputDimensions); + read_little_endian(stream, psqtWeights, PSQTBuckets * InputDimensions); + + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + + write_little_endian(stream, biases , HalfDimensions ); + write_little_endian(stream, weights , HalfDimensions * InputDimensions); + write_little_endian(stream, psqtWeights, PSQTBuckets * InputDimensions); + + return !stream.fail(); + } + + // Convert input features + std::int32_t transform(const Position& pos, OutputType* output, int bucket) const { + update_accumulator(pos, WHITE); + update_accumulator(pos, BLACK); + + const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()}; + const auto& accumulation = pos.state()->accumulator.accumulation; + const auto& psqtAccumulation = pos.state()->accumulator.psqtAccumulation; + + const auto psqt = ( + psqtAccumulation[perspectives[0]][bucket] + - psqtAccumulation[perspectives[1]][bucket] + ) / 2; + + + #if defined(USE_AVX512) + + constexpr IndexType NumChunks = HalfDimensions / (SimdWidth * 2); + static_assert(HalfDimensions % (SimdWidth * 2) == 0); + const __m512i Control = _mm512_setr_epi64(0, 2, 4, 6, 1, 3, 5, 7); + const __m512i Zero = _mm512_setzero_si512(); + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + auto out = reinterpret_cast<__m512i*>(&output[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + __m512i sum0 = _mm512_load_si512(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 0]); + __m512i sum1 = _mm512_load_si512(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 1]); + + _mm512_store_si512(&out[j], _mm512_permutexvar_epi64(Control, + _mm512_max_epi8(_mm512_packs_epi16(sum0, sum1), Zero))); + } + } + return psqt; + + #elif defined(USE_AVX2) + + constexpr IndexType NumChunks = HalfDimensions / SimdWidth; + constexpr int Control = 0b11011000; + const __m256i Zero = _mm256_setzero_si256(); + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + auto out = reinterpret_cast<__m256i*>(&output[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + __m256i sum0 = _mm256_load_si256(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 0]); + __m256i sum1 = _mm256_load_si256(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 1]); + + _mm256_store_si256(&out[j], _mm256_permute4x64_epi64( + _mm256_max_epi8(_mm256_packs_epi16(sum0, sum1), Zero), Control)); + } + } + return psqt; + + #elif defined(USE_SSE2) + + #ifdef USE_SSE41 + constexpr IndexType NumChunks = HalfDimensions / SimdWidth; + const __m128i Zero = _mm_setzero_si128(); + #else + constexpr IndexType NumChunks = HalfDimensions / SimdWidth; + const __m128i k0x80s = _mm_set1_epi8(-128); + #endif + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + auto out = reinterpret_cast<__m128i*>(&output[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + __m128i sum0 = _mm_load_si128(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 0]); + __m128i sum1 = _mm_load_si128(&reinterpret_cast + (accumulation[perspectives[p]])[j * 2 + 1]); + const __m128i packedbytes = _mm_packs_epi16(sum0, sum1); + + #ifdef USE_SSE41 + _mm_store_si128(&out[j], _mm_max_epi8(packedbytes, Zero)); + #else + _mm_store_si128(&out[j], _mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s)); + #endif + } + } + return psqt; + + #elif defined(USE_MMX) + + constexpr IndexType NumChunks = HalfDimensions / SimdWidth; + const __m64 k0x80s = _mm_set1_pi8(-128); + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + auto out = reinterpret_cast<__m64*>(&output[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + __m64 sum0 = *(&reinterpret_cast(accumulation[perspectives[p]])[j * 2 + 0]); + __m64 sum1 = *(&reinterpret_cast(accumulation[perspectives[p]])[j * 2 + 1]); + const __m64 packedbytes = _mm_packs_pi16(sum0, sum1); + out[j] = _mm_subs_pi8(_mm_adds_pi8(packedbytes, k0x80s), k0x80s); + } + } + _mm_empty(); + return psqt; + + #elif defined(USE_NEON) + + constexpr IndexType NumChunks = HalfDimensions / (SimdWidth / 2); + const int8x8_t Zero = {0}; + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + const auto out = reinterpret_cast(&output[offset]); + + constexpr IndexType UnrollFactor = 16; + static_assert(UnrollFactor % UnrollFactor == 0); + for (IndexType j = 0; j < NumChunks; j += UnrollFactor) + { + int16x8_t sums[UnrollFactor]; + for (IndexType i = 0; i < UnrollFactor; ++i) + sums[i] = reinterpret_cast(accumulation[perspectives[p]])[j+i]; + + for (IndexType i = 0; i < UnrollFactor; ++i) + out[j+i] = vmax_s8(vqmovn_s16(sums[i]), Zero); + } + } + return psqt; + + #else + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = HalfDimensions * p; + for (IndexType j = 0; j < HalfDimensions; ++j) + { + BiasType sum = accumulation[perspectives[p]][j]; + output[offset + j] = static_cast(std::max(0, std::min(127, sum))); + } + } + return psqt; + + #endif + + } // end of function transform() + + + + private: + void update_accumulator(const Position& pos, const Color perspective) const { + + // The size must be enough to contain the largest possible update. + // That might depend on the feature set and generally relies on the + // feature set's update cost calculation to be correct and never + // allow updates with more added/removed features than MaxActiveDimensions. + + #ifdef VECTOR + // Gcc-10.2 unnecessarily spills AVX2 registers if this array + // is defined in the VECTOR code below, once in each branch + vec_t acc[NumRegs]; + psqt_vec_t psqt[NumPsqtRegs]; + #endif + + // Look for a usable accumulator of an earlier position. We keep track + // of the estimated gain in terms of features to be added/subtracted. + StateInfo *st = pos.state(), *next = nullptr; + int gain = FeatureSet::refresh_cost(pos); + while (st->previous && !st->accumulator.computed[perspective]) + { + // This governs when a full feature refresh is needed and how many + // updates are better than just one full refresh. + if ( FeatureSet::requires_refresh(st, perspective) + || (gain -= FeatureSet::update_cost(st) + 1) < 0) + break; + next = st; + st = st->previous; + } + + if (st->accumulator.computed[perspective]) + { + if (next == nullptr) + return; + + // Update incrementally in two steps. First, we update the "next" + // accumulator. Then, we update the current accumulator (pos.state()). + + // Gather all features to be updated. + const Square ksq = pos.square(perspective); + FeatureSet::IndexList removed[2], added[2]; + FeatureSet::append_changed_indices( + ksq, next->dirtyPiece, perspective, removed[0], added[0]); + for (StateInfo *st2 = pos.state(); st2 != next; st2 = st2->previous) + FeatureSet::append_changed_indices( + ksq, st2->dirtyPiece, perspective, removed[1], added[1]); + + // Mark the accumulators as computed. + next->accumulator.computed[perspective] = true; + pos.state()->accumulator.computed[perspective] = true; + + // Now update the accumulators listed in states_to_update[], where the last element is a sentinel. + StateInfo *states_to_update[3] = + { next, next == pos.state() ? nullptr : pos.state(), nullptr }; + #ifdef VECTOR + for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) + { + // Load accumulator + auto accTile = reinterpret_cast( + &st->accumulator.accumulation[perspective][j * TileHeight]); + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = vec_load(&accTile[k]); + + for (IndexType i = 0; states_to_update[i]; ++i) + { + // Difference calculation for the deactivated features + for (const auto index : removed[i]) + { + const IndexType offset = HalfDimensions * index + j * TileHeight; + auto column = reinterpret_cast(&weights[offset]); + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = vec_sub_16(acc[k], column[k]); + } + + // Difference calculation for the activated features + for (const auto index : added[i]) + { + const IndexType offset = HalfDimensions * index + j * TileHeight; + auto column = reinterpret_cast(&weights[offset]); + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = vec_add_16(acc[k], column[k]); + } + + // Store accumulator + accTile = reinterpret_cast( + &states_to_update[i]->accumulator.accumulation[perspective][j * TileHeight]); + for (IndexType k = 0; k < NumRegs; ++k) + vec_store(&accTile[k], acc[k]); + } + } + + for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) + { + // Load accumulator + auto accTilePsqt = reinterpret_cast( + &st->accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]); + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + psqt[k] = vec_load_psqt(&accTilePsqt[k]); + + for (IndexType i = 0; states_to_update[i]; ++i) + { + // Difference calculation for the deactivated features + for (const auto index : removed[i]) + { + const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; + auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); + } + + // Difference calculation for the activated features + for (const auto index : added[i]) + { + const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; + auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + // Store accumulator + accTilePsqt = reinterpret_cast( + &states_to_update[i]->accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]); + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + vec_store_psqt(&accTilePsqt[k], psqt[k]); + } + } + + #else + for (IndexType i = 0; states_to_update[i]; ++i) + { + std::memcpy(states_to_update[i]->accumulator.accumulation[perspective], + st->accumulator.accumulation[perspective], + HalfDimensions * sizeof(BiasType)); + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + states_to_update[i]->accumulator.psqtAccumulation[perspective][k] = st->accumulator.psqtAccumulation[perspective][k]; + + st = states_to_update[i]; + + // Difference calculation for the deactivated features + for (const auto index : removed[i]) + { + const IndexType offset = HalfDimensions * index; + + for (IndexType j = 0; j < HalfDimensions; ++j) + st->accumulator.accumulation[perspective][j] -= weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + st->accumulator.psqtAccumulation[perspective][k] -= psqtWeights[index * PSQTBuckets + k]; + } + + // Difference calculation for the activated features + for (const auto index : added[i]) + { + const IndexType offset = HalfDimensions * index; + + for (IndexType j = 0; j < HalfDimensions; ++j) + st->accumulator.accumulation[perspective][j] += weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + st->accumulator.psqtAccumulation[perspective][k] += psqtWeights[index * PSQTBuckets + k]; + } + } + #endif + } + else + { + // Refresh the accumulator + auto& accumulator = pos.state()->accumulator; + accumulator.computed[perspective] = true; + FeatureSet::IndexList active; + FeatureSet::append_active_indices(pos, perspective, active); + + #ifdef VECTOR + for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) + { + auto biasesTile = reinterpret_cast( + &biases[j * TileHeight]); + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = biasesTile[k]; + + for (const auto index : active) + { + const IndexType offset = HalfDimensions * index + j * TileHeight; + auto column = reinterpret_cast(&weights[offset]); + + for (unsigned k = 0; k < NumRegs; ++k) + acc[k] = vec_add_16(acc[k], column[k]); + } + + auto accTile = reinterpret_cast( + &accumulator.accumulation[perspective][j * TileHeight]); + for (unsigned k = 0; k < NumRegs; k++) + vec_store(&accTile[k], acc[k]); + } + + for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) + { + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + psqt[k] = vec_zero_psqt(); + + for (const auto index : active) + { + const IndexType offset = PSQTBuckets * index + j * PsqtTileHeight; + auto columnPsqt = reinterpret_cast(&psqtWeights[offset]); + + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + auto accTilePsqt = reinterpret_cast( + &accumulator.psqtAccumulation[perspective][j * PsqtTileHeight]); + for (std::size_t k = 0; k < NumPsqtRegs; ++k) + vec_store_psqt(&accTilePsqt[k], psqt[k]); + } + + #else + std::memcpy(accumulator.accumulation[perspective], biases, + HalfDimensions * sizeof(BiasType)); + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + accumulator.psqtAccumulation[perspective][k] = 0; + + for (const auto index : active) + { + const IndexType offset = HalfDimensions * index; + + for (IndexType j = 0; j < HalfDimensions; ++j) + accumulator.accumulation[perspective][j] += weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + accumulator.psqtAccumulation[perspective][k] += psqtWeights[index * PSQTBuckets + k]; + } + #endif + } + + #if defined(USE_MMX) + _mm_empty(); + #endif + } + + alignas(CacheLineSize) BiasType biases[HalfDimensions]; + alignas(CacheLineSize) WeightType weights[HalfDimensions * InputDimensions]; + alignas(CacheLineSize) PSQTWeightType psqtWeights[InputDimensions * PSQTBuckets]; + }; + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED diff --git a/src/pawns.cpp b/src/pawns.cpp index c3f7872f9e7..6e509133abc 100644 --- a/src/pawns.cpp +++ b/src/pawns.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,29 +24,38 @@ #include "position.h" #include "thread.h" +namespace Stockfish { + namespace { #define V Value #define S(mg, eg) make_score(mg, eg) // Pawn penalties - constexpr Score Backward = S( 9, 24); - constexpr Score BlockedStorm = S(82, 82); - constexpr Score Doubled = S(11, 56); - constexpr Score Isolated = S( 5, 15); - constexpr Score WeakLever = S( 0, 56); - constexpr Score WeakUnopposed = S(13, 27); + constexpr Score Backward = S( 9, 22); + constexpr Score Doubled = S(13, 51); + constexpr Score DoubledEarly = S(20, 7); + constexpr Score Isolated = S( 3, 15); + constexpr Score WeakLever = S( 4, 58); + constexpr Score WeakUnopposed = S(13, 24); + + // Bonus for blocked pawns at 5th or 6th rank + constexpr Score BlockedPawn[2] = { S(-17, -6), S(-9, 2) }; + + constexpr Score BlockedStorm[RANK_NB] = { + S(0, 0), S(0, 0), S(75, 78), S(-8, 16), S(-6, 10), S(-6, 6), S(0, 2) + }; // Connected pawn bonus - constexpr int Connected[RANK_NB] = { 0, 7, 8, 12, 29, 48, 86 }; + constexpr int Connected[RANK_NB] = { 0, 5, 7, 11, 23, 48, 87 }; // Strength of pawn shelter for our king by [distance from edge][rank]. // RANK_1 = 0 is used for files where we have no pawn, or pawn is behind our king. constexpr Value ShelterStrength[int(FILE_NB) / 2][RANK_NB] = { - { V( -6), V( 81), V( 93), V( 58), V( 39), V( 18), V( 25) }, - { V(-43), V( 61), V( 35), V(-49), V(-29), V(-11), V( -63) }, - { V(-10), V( 75), V( 23), V( -2), V( 32), V( 3), V( -45) }, - { V(-39), V(-13), V(-29), V(-52), V(-48), V(-67), V(-166) } + { V( -5), V( 82), V( 92), V( 54), V( 36), V( 22), V( 28) }, + { V(-44), V( 63), V( 33), V(-50), V(-30), V(-12), V( -62) }, + { V(-11), V( 77), V( 22), V( -6), V( 31), V( 8), V( -45) }, + { V(-39), V(-12), V(-29), V(-50), V(-43), V(-68), V(-164) } }; // Danger of enemy pawns moving toward our king by [distance from edge][rank]. @@ -56,27 +63,40 @@ namespace { // is behind our king. Note that UnblockedStorm[0][1-2] accommodate opponent pawn // on edge, likely blocked by our king. constexpr Value UnblockedStorm[int(FILE_NB) / 2][RANK_NB] = { - { V( 85), V(-289), V(-166), V(97), V(50), V( 45), V( 50) }, - { V( 46), V( -25), V( 122), V(45), V(37), V(-10), V( 20) }, - { V( -6), V( 51), V( 168), V(34), V(-2), V(-22), V(-14) }, - { V(-15), V( -11), V( 101), V( 4), V(11), V(-15), V(-29) } + { V( 87), V(-288), V(-168), V( 96), V( 47), V( 44), V( 46) }, + { V( 42), V( -25), V( 120), V( 45), V( 34), V( -9), V( 24) }, + { V( -8), V( 51), V( 167), V( 35), V( -4), V(-16), V(-12) }, + { V(-17), V( -13), V( 100), V( 4), V( 9), V(-16), V(-31) } }; + + // KingOnFile[semi-open Us][semi-open Them] contains bonuses/penalties + // for king when the king is on a semi-open or open file. + constexpr Score KingOnFile[2][2] = {{ S(-21,10), S(-7, 1) }, + { S( 0,-3), S( 9,-4) }}; + #undef S #undef V + + /// evaluate() calculates a score for the static pawn structure of the given position. + /// We cannot use the location of pieces or king in this function, as the evaluation + /// of the pawn structure will be stored in a small cache for speed reasons, and will + /// be re-used even when the pieces have moved. + template Score evaluate(const Position& pos, Pawns::Entry* e) { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; constexpr Direction Up = pawn_push(Us); + constexpr Direction Down = -Up; Bitboard neighbours, stoppers, support, phalanx, opposed; Bitboard lever, leverPush, blocked; Square s; bool backward, passed, doubled; Score score = SCORE_ZERO; - const Square* pl = pos.squares(Us); + Bitboard b = pos.pieces(Us, PAWN); Bitboard ourPawns = pos.pieces( Us, PAWN); Bitboard theirPawns = pos.pieces(Them, PAWN); @@ -86,10 +106,13 @@ namespace { e->passedPawns[Us] = 0; e->kingSquares[Us] = SQ_NONE; e->pawnAttacks[Us] = e->pawnAttacksSpan[Us] = pawn_attacks_bb(ourPawns); + e->blockedCount += popcount(shift(ourPawns) & (theirPawns | doubleAttackThem)); // Loop through all pawns of the current color and score each pawn - while ((s = *pl++) != SQ_NONE) + while (b) { + s = pop_lsb(b); + assert(pos.piece_on(s) == make_piece(Us, PAWN)); Rank r = relative_rank(Us, s); @@ -98,13 +121,20 @@ namespace { opposed = theirPawns & forward_file_bb(Us, s); blocked = theirPawns & (s + Up); stoppers = theirPawns & passed_pawn_span(Us, s); - lever = theirPawns & PawnAttacks[Us][s]; - leverPush = theirPawns & PawnAttacks[Us][s + Up]; + lever = theirPawns & pawn_attacks_bb(Us, s); + leverPush = theirPawns & pawn_attacks_bb(Us, s + Up); doubled = ourPawns & (s - Up); neighbours = ourPawns & adjacent_files_bb(s); phalanx = neighbours & rank_bb(s); support = neighbours & rank_bb(s - Up); + if (doubled) + { + // Additional doubled penalty if none of their pawns is fixed + if (!(ourPawns & shift(theirPawns | pawn_attacks_bb(theirPawns)))) + score -= DoubledEarly; + } + // A pawn is backward when it is behind all pawns of the same color on // the adjacent files and cannot safely advance. backward = !(neighbours & forward_ranks_bb(Them, s + Up)) @@ -118,12 +148,15 @@ namespace { // (a) there is no stoppers except some levers // (b) the only stoppers are the leverPush, but we outnumber them // (c) there is only one front stopper which can be levered. + // (Refined in Evaluation::passed) passed = !(stoppers ^ lever) || ( !(stoppers ^ leverPush) && popcount(phalanx) >= popcount(leverPush)) || ( stoppers == blocked && r >= RANK_5 && (shift(support) & ~(theirPawns | doubleAttackThem))); + passed &= !(forward_file_bb(Us, s) & ourPawns); + // Passed pawns will be properly scored later in evaluation when we have // full attack info. if (passed) @@ -133,22 +166,32 @@ namespace { if (support | phalanx) { int v = Connected[r] * (2 + bool(phalanx) - bool(opposed)) - + 21 * popcount(support); + + 22 * popcount(support); score += make_score(v, v * (r - 2) / 4); } else if (!neighbours) - score -= Isolated - + WeakUnopposed * !opposed; + { + if ( opposed + && (ourPawns & forward_file_bb(Them, s)) + && !(theirPawns & adjacent_files_bb(s))) + score -= Doubled; + else + score -= Isolated + + WeakUnopposed * !opposed; + } else if (backward) - score -= Backward - + WeakUnopposed * !opposed; + score -= Backward + + WeakUnopposed * !opposed * bool(~(FileABB | FileHBB) & s); if (!support) - score -= Doubled * doubled - + WeakLever * more_than_one(lever); + score -= Doubled * doubled + + WeakLever * more_than_one(lever); + + if (blocked && r >= RANK_5) + score += BlockedPawn[r - RANK_5]; } return score; @@ -158,6 +201,7 @@ namespace { namespace Pawns { + /// Pawns::probe() looks up the current position's pawns configuration in /// the pawns hash table. It returns a pointer to the Entry if the position /// is found. Otherwise a new Entry is computed and stored there, so we don't @@ -172,6 +216,7 @@ Entry* probe(const Position& pos) { return e; e->key = key; + e->blockedCount = 0; e->scores[WHITE] = evaluate(pos, e); e->scores[BLACK] = evaluate(pos, e); @@ -183,17 +228,17 @@ Entry* probe(const Position& pos) { /// penalty for a king, looking at the king file and the two closest files. template -Score Entry::evaluate_shelter(const Position& pos, Square ksq) { +Score Entry::evaluate_shelter(const Position& pos, Square ksq) const { - constexpr Color Them = (Us == WHITE ? BLACK : WHITE); + constexpr Color Them = ~Us; Bitboard b = pos.pieces(PAWN) & ~forward_ranks_bb(Them, ksq); - Bitboard ourPawns = b & pos.pieces(Us); + Bitboard ourPawns = b & pos.pieces(Us) & ~pawnAttacks[Them]; Bitboard theirPawns = b & pos.pieces(Them); Score bonus = make_score(5, 5); - File center = clamp(file_of(ksq), FILE_B, FILE_G); + File center = std::clamp(file_of(ksq), FILE_B, FILE_G); for (File f = File(center - 1); f <= File(center + 1); ++f) { b = ourPawns & file_bb(f); @@ -202,15 +247,18 @@ Score Entry::evaluate_shelter(const Position& pos, Square ksq) { b = theirPawns & file_bb(f); int theirRank = b ? relative_rank(Us, frontmost_sq(Them, b)) : 0; - File d = map_to_queenside(f); + int d = edge_distance(f); bonus += make_score(ShelterStrength[d][ourRank], 0); if (ourRank && (ourRank == theirRank - 1)) - bonus -= BlockedStorm * int(theirRank == RANK_3); + bonus -= BlockedStorm[theirRank]; else bonus -= make_score(UnblockedStorm[d][theirRank], 0); } + // King On File + bonus -= KingOnFile[pos.is_on_semiopen_file(Us, ksq)][pos.is_on_semiopen_file(Them, ksq)]; + return bonus; } @@ -238,12 +286,12 @@ Score Entry::do_king_safety(const Position& pos) { // In endgame we like to bring our king near our closest pawn Bitboard pawns = pos.pieces(Us, PAWN); - int minPawnDist = pawns ? 8 : 0; + int minPawnDist = 6; - if (pawns & PseudoAttacks[KING][ksq]) + if (pawns & attacks_bb(ksq)) minPawnDist = 1; else while (pawns) - minPawnDist = std::min(minPawnDist, distance(ksq, pop_lsb(&pawns))); + minPawnDist = std::min(minPawnDist, distance(ksq, pop_lsb(pawns))); return shelter - make_score(0, 16 * minPawnDist); } @@ -253,3 +301,5 @@ template Score Entry::do_king_safety(const Position& pos); template Score Entry::do_king_safety(const Position& pos); } // namespace Pawns + +} // namespace Stockfish diff --git a/src/pawns.h b/src/pawns.h index bd17618f5d8..af0370fc41a 100644 --- a/src/pawns.h +++ b/src/pawns.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,7 +23,7 @@ #include "position.h" #include "types.h" -namespace Pawns { +namespace Stockfish::Pawns { /// Pawns::Entry contains various information about a pawn structure. A lookup /// to the pawn hash table (performed by calling the probe function) returns a @@ -38,6 +36,7 @@ struct Entry { Bitboard passed_pawns(Color c) const { return passedPawns[c]; } Bitboard pawn_attacks_span(Color c) const { return pawnAttacksSpan[c]; } int passed_count() const { return popcount(passedPawns[WHITE] | passedPawns[BLACK]); } + int blocked_count() const { return blockedCount; } template Score king_safety(const Position& pos) { @@ -49,7 +48,7 @@ struct Entry { Score do_king_safety(const Position& pos); template - Score evaluate_shelter(const Position& pos, Square ksq); + Score evaluate_shelter(const Position& pos, Square ksq) const; Key key; Score scores[COLOR_NB]; @@ -59,12 +58,13 @@ struct Entry { Square kingSquares[COLOR_NB]; Score kingSafety[COLOR_NB]; int castlingRights[COLOR_NB]; + int blockedCount; }; typedef HashTable Table; Entry* probe(const Position& pos); -} // namespace Pawns +} // namespace Stockfish::Pawns #endif // #ifndef PAWNS_H_INCLUDED diff --git a/src/position.cpp b/src/position.cpp index 53d9b64e9ff..ec9229ea337 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,6 +34,8 @@ using std::string; +namespace Stockfish { + namespace Zobrist { Key psq[PIECE_NB][SQUARE_NB]; @@ -64,20 +64,23 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) { for (File f = FILE_A; f <= FILE_H; ++f) os << " | " << PieceToChar[pos.piece_on(make_square(f, r))]; - os << " |\n +---+---+---+---+---+---+---+---+\n"; + os << " | " << (1 + r) << "\n +---+---+---+---+---+---+---+---+\n"; } - os << "\nFen: " << pos.fen() << "\nKey: " << std::hex << std::uppercase + os << " a b c d e f g h\n" + << "\nFen: " << pos.fen() << "\nKey: " << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << pos.key() << std::setfill(' ') << std::dec << "\nCheckers: "; for (Bitboard b = pos.checkers(); b; ) - os << UCI::square(pop_lsb(&b)) << " "; + os << UCI::square(pop_lsb(b)) << " "; if ( int(Tablebases::MaxCardinality) >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING)) { StateInfo st; + ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); + Position p; p.set(pos.fen(), pos.is_chess960(), &st, pos.this_thread()); Tablebases::ProbeState s1, s2; @@ -104,8 +107,7 @@ Key cuckoo[8192]; Move cuckooMove[8192]; -/// Position::init() initializes at startup the various arrays used to compute -/// hash keys. +/// Position::init() initializes at startup the various arrays used to compute hash keys void Position::init() { @@ -119,15 +121,7 @@ void Position::init() { Zobrist::enpassant[f] = rng.rand(); for (int cr = NO_CASTLING; cr <= ANY_CASTLING; ++cr) - { - Zobrist::castling[cr] = 0; - Bitboard b = cr; - while (b) - { - Key k = Zobrist::castling[1ULL << pop_lsb(&b)]; - Zobrist::castling[cr] ^= k ? k : rng.rand(); - } - } + Zobrist::castling[cr] = rng.rand(); Zobrist::side = rng.rand(); Zobrist::noPawns = rng.rand(); @@ -139,7 +133,7 @@ void Position::init() { for (Piece pc : Pieces) for (Square s1 = SQ_A1; s1 <= SQ_H8; ++s1) for (Square s2 = Square(s1 + 1); s2 <= SQ_H8; ++s2) - if (PseudoAttacks[type_of(pc)][s1] & s2) + if ((type_of(pc) != PAWN) && (attacks_bb(type_of(pc), s1, 0) & s2)) { Move move = make_move(s1, s2); Key key = Zobrist::psq[pc][s1] ^ Zobrist::psq[pc][s2] ^ Zobrist::side; @@ -186,9 +180,9 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th 4) En passant target square (in algebraic notation). If there's no en passant target square, this is "-". If a pawn has just made a 2-square move, this - is the position "behind" the pawn. This is recorded only if there is a pawn - in position to make an en passant capture, and if there really is a pawn - that might have advanced two squares. + is the position "behind" the pawn. Following X-FEN standard, this is recorded only + if there is a pawn in position to make an en passant capture, and if there really + is a pawn that might have advanced two squares. 5) Halfmove clock. This is the number of halfmoves since the last pawn advance or capture. This is used to determine if a draw can be claimed under the @@ -205,7 +199,6 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th std::memset(this, 0, sizeof(Position)); std::memset(si, 0, sizeof(StateInfo)); - std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE); st = si; ss >> std::noskipws; @@ -219,8 +212,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th else if (token == '/') sq += 2 * SOUTH; - else if ((idx = PieceToChar.find(token)) != string::npos) - { + else if ((idx = PieceToChar.find(token)) != string::npos) { put_piece(Piece(idx), sq); ++sq; } @@ -259,17 +251,25 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th set_castling_right(c, rsq); } - // 4. En passant square. Ignore if no pawn capture is possible + // 4. En passant square. + // Ignore if square is invalid or not on side to move relative rank 6. + bool enpassant = false; + if ( ((ss >> col) && (col >= 'a' && col <= 'h')) - && ((ss >> row) && (row == '3' || row == '6'))) + && ((ss >> row) && (row == (sideToMove == WHITE ? '6' : '3')))) { st->epSquare = make_square(File(col - 'a'), Rank(row - '1')); - if ( !(attackers_to(st->epSquare) & pieces(sideToMove, PAWN)) - || !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove)))) - st->epSquare = SQ_NONE; + // En passant square will be considered only if + // a) side to move have a pawn threatening epSquare + // b) there is an enemy pawn in front of epSquare + // c) there is no piece on epSquare or behind epSquare + enpassant = pawn_attacks_bb(~sideToMove, st->epSquare) & pieces(sideToMove, PAWN) + && (pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove))) + && !(pieces() & (st->epSquare | (st->epSquare + pawn_push(sideToMove)))); } - else + + if (!enpassant) st->epSquare = SQ_NONE; // 5-6. Halfmove clock and fullmove number @@ -305,8 +305,8 @@ void Position::set_castling_right(Color c, Square rfrom) { Square kto = relative_square(c, cr & KING_SIDE ? SQ_G1 : SQ_C1); Square rto = relative_square(c, cr & KING_SIDE ? SQ_F1 : SQ_D1); - castlingPath[cr] = (between_bb(rfrom, rto) | between_bb(kfrom, kto) | rto | kto) - & ~(square_bb(kfrom) | rfrom); + castlingPath[cr] = (between_bb(rfrom, rto) | between_bb(kfrom, kto)) + & ~(kfrom | rfrom); } @@ -319,10 +319,10 @@ void Position::set_check_info(StateInfo* si) const { Square ksq = square(~sideToMove); - si->checkSquares[PAWN] = attacks_from(ksq, ~sideToMove); - si->checkSquares[KNIGHT] = attacks_from(ksq); - si->checkSquares[BISHOP] = attacks_from(ksq); - si->checkSquares[ROOK] = attacks_from(ksq); + si->checkSquares[PAWN] = pawn_attacks_bb(~sideToMove, ksq); + si->checkSquares[KNIGHT] = attacks_bb(ksq); + si->checkSquares[BISHOP] = attacks_bb(ksq, pieces()); + si->checkSquares[ROOK] = attacks_bb(ksq, pieces()); si->checkSquares[QUEEN] = si->checkSquares[BISHOP] | si->checkSquares[ROOK]; si->checkSquares[KING] = 0; } @@ -344,7 +344,7 @@ void Position::set_state(StateInfo* si) const { for (Bitboard b = pieces(); b; ) { - Square s = pop_lsb(&b); + Square s = pop_lsb(b); Piece pc = piece_on(s); si->key ^= Zobrist::psq[pc][s]; @@ -375,11 +375,13 @@ void Position::set_state(StateInfo* si) const { Position& Position::set(const string& code, Color c, StateInfo* si) { - assert(code.length() > 0 && code.length() < 8); assert(code[0] == 'K'); string sides[] = { code.substr(code.find('K', 1)), // Weak - code.substr(0, code.find('K', 1)) }; // Strong + code.substr(0, std::min(code.find('v'), code.find('K', 1))) }; // Strong + + assert(sides[0].length() > 0 && sides[0].length() < 8); + assert(sides[1].length() > 0 && sides[1].length() < 8); std::transform(sides[c].begin(), sides[c].end(), sides[c].begin(), tolower); @@ -393,7 +395,7 @@ Position& Position::set(const string& code, Color c, StateInfo* si) { /// Position::fen() returns a FEN representation of the position. In case of /// Chess960 the Shredder-FEN notation is used. This is mainly a debugging function. -const string Position::fen() const { +string Position::fen() const { int emptyCnt; std::ostringstream ss; @@ -453,13 +455,13 @@ Bitboard Position::slider_blockers(Bitboard sliders, Square s, Bitboard& pinners pinners = 0; // Snipers are sliders that attack 's' when a piece and other snipers are removed - Bitboard snipers = ( (PseudoAttacks[ ROOK][s] & pieces(QUEEN, ROOK)) - | (PseudoAttacks[BISHOP][s] & pieces(QUEEN, BISHOP))) & sliders; + Bitboard snipers = ( (attacks_bb< ROOK>(s) & pieces(QUEEN, ROOK)) + | (attacks_bb(s) & pieces(QUEEN, BISHOP))) & sliders; Bitboard occupancy = pieces() ^ snipers; while (snipers) { - Square sniperSq = pop_lsb(&snipers); + Square sniperSq = pop_lsb(snipers); Bitboard b = between_bb(s, sniperSq) & occupancy; if (b && !more_than_one(b)) @@ -478,12 +480,12 @@ Bitboard Position::slider_blockers(Bitboard sliders, Square s, Bitboard& pinners Bitboard Position::attackers_to(Square s, Bitboard occupied) const { - return (attacks_from(s, BLACK) & pieces(WHITE, PAWN)) - | (attacks_from(s, WHITE) & pieces(BLACK, PAWN)) - | (attacks_from(s) & pieces(KNIGHT)) + return (pawn_attacks_bb(BLACK, s) & pieces(WHITE, PAWN)) + | (pawn_attacks_bb(WHITE, s) & pieces(BLACK, PAWN)) + | (attacks_bb(s) & pieces(KNIGHT)) | (attacks_bb< ROOK>(s, occupied) & pieces( ROOK, QUEEN)) | (attacks_bb(s, occupied) & pieces(BISHOP, QUEEN)) - | (attacks_from(s) & pieces(KING)); + | (attacks_bb(s) & pieces(KING)); } @@ -503,7 +505,7 @@ bool Position::legal(Move m) const { // En passant captures are a tricky special case. Because they are rather // uncommon, we do it simply by testing whether the king is attacked after // the move is made. - if (type_of(m) == ENPASSANT) + if (type_of(m) == EN_PASSANT) { Square ksq = square(us); Square capsq = to - pawn_push(us); @@ -531,22 +533,20 @@ bool Position::legal(Move m) const { if (attackers_to(s) & pieces(~us)) return false; - // In case of Chess960, verify that when moving the castling rook we do - // not discover some hidden checker. + // In case of Chess960, verify if the Rook blocks some checks // For instance an enemy queen in SQ_A1 when castling rook is in SQ_B1. - return !chess960 - || !(attacks_bb(to, pieces() ^ to_sq(m)) & pieces(~us, ROOK, QUEEN)); + return !chess960 || !(blockers_for_king(us) & to_sq(m)); } // If the moving piece is a king, check whether the destination square is // attacked by the opponent. if (type_of(piece_on(from)) == KING) - return !(attackers_to(to) & pieces(~us)); + return !(attackers_to(to, pieces() ^ from) & pieces(~us)); // A non-king move is legal if and only if it is not pinned or it // is moving along the ray towards or away from the king. - return !(blockers_for_king(us) & from) - || aligned(from, to, square(us)); + return !(blockers_for_king(us) & from) + || aligned(from, to, square(us)); } @@ -562,8 +562,10 @@ bool Position::pseudo_legal(const Move m) const { Piece pc = moved_piece(m); // Use a slower but simpler function for uncommon cases + // yet we skip the legality check of MoveList(). if (type_of(m) != NORMAL) - return MoveList(*this).contains(m); + return checkers() ? MoveList< EVASIONS>(*this).contains(m) + : MoveList(*this).contains(m); // Is not a promotion, so promotion piece must be empty if (promotion_type(m) - KNIGHT != NO_PIECE_TYPE) @@ -586,15 +588,15 @@ bool Position::pseudo_legal(const Move m) const { if ((Rank8BB | Rank1BB) & to) return false; - if ( !(attacks_from(from, us) & pieces(~us) & to) // Not a capture + if ( !(pawn_attacks_bb(us, from) & pieces(~us) & to) // Not a capture && !((from + pawn_push(us) == to) && empty(to)) // Not a single push && !( (from + 2 * pawn_push(us) == to) // Not a double push - && (rank_of(from) == relative_rank(us, RANK_2)) + && (relative_rank(us, from) == RANK_2) && empty(to) && empty(to - pawn_push(us)))) return false; } - else if (!(attacks_from(type_of(pc), from) & to)) + else if (!(attacks_bb(type_of(pc), from, pieces()) & to)) return false; // Evasions generator already takes care to avoid some kind of illegal moves @@ -608,8 +610,8 @@ bool Position::pseudo_legal(const Move m) const { if (more_than_one(checkers())) return false; - // Our move must be a blocking evasion or a capture of the checking piece - if (!((between_bb(lsb(checkers()), square(us)) | checkers()) & to)) + // Our move must be a blocking interposition or a capture of the checking piece + if (!(between_bb(square(us), lsb(checkers())) & to)) return false; } // In case of king moves under check we have to remove king so as to catch @@ -633,11 +635,11 @@ bool Position::gives_check(Move m) const { Square to = to_sq(m); // Is there a direct check? - if (st->checkSquares[type_of(piece_on(from))] & to) + if (check_squares(type_of(piece_on(from))) & to) return true; // Is there a discovered check? - if ( (st->blockersForKing[~sideToMove] & from) + if ( (blockers_for_king(~sideToMove) & from) && !aligned(from, to, square(~sideToMove))) return true; @@ -653,7 +655,7 @@ bool Position::gives_check(Move m) const { // of direct checks and ordinary discovered check, so the only case we // need to handle is the unusual case of a discovered check through // the captured pawn. - case ENPASSANT: + case EN_PASSANT: { Square capsq = make_square(file_of(to), rank_of(from)); Bitboard b = (pieces() ^ from ^ capsq) | to; @@ -661,19 +663,15 @@ bool Position::gives_check(Move m) const { return (attacks_bb< ROOK>(square(~sideToMove), b) & pieces(sideToMove, QUEEN, ROOK)) | (attacks_bb(square(~sideToMove), b) & pieces(sideToMove, QUEEN, BISHOP)); } - case CASTLING: + default: //CASTLING { - Square kfrom = from; - Square rfrom = to; // Castling is encoded as 'King captures the rook' - Square kto = relative_square(sideToMove, rfrom > kfrom ? SQ_G1 : SQ_C1); - Square rto = relative_square(sideToMove, rfrom > kfrom ? SQ_F1 : SQ_D1); + // Castling is encoded as 'king captures the rook' + Square ksq = square(~sideToMove); + Square rto = relative_square(sideToMove, to > from ? SQ_F1 : SQ_D1); - return (PseudoAttacks[ROOK][rto] & square(~sideToMove)) - && (attacks_bb(rto, (pieces() ^ kfrom ^ rfrom) | rto | kto) & square(~sideToMove)); + return (attacks_bb(rto) & ksq) + && (attacks_bb(rto, pieces() ^ from ^ to) & ksq); } - default: - assert(false); - return false; } } @@ -703,12 +701,18 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { ++st->rule50; ++st->pliesFromNull; + // Used by NNUE + st->accumulator.computed[WHITE] = false; + st->accumulator.computed[BLACK] = false; + auto& dp = st->dirtyPiece; + dp.dirty_num = 1; + Color us = sideToMove; Color them = ~us; Square from = from_sq(m); Square to = to_sq(m); Piece pc = piece_on(from); - Piece captured = type_of(m) == ENPASSANT ? make_piece(them, PAWN) : piece_on(to); + Piece captured = type_of(m) == EN_PASSANT ? make_piece(them, PAWN) : piece_on(to); assert(color_of(pc) == us); assert(captured == NO_PIECE || color_of(captured) == (type_of(m) != CASTLING ? them : us)); @@ -734,7 +738,7 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // update non-pawn material. if (type_of(captured) == PAWN) { - if (type_of(m) == ENPASSANT) + if (type_of(m) == EN_PASSANT) { capsq -= pawn_push(us); @@ -743,8 +747,6 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(relative_rank(us, to) == RANK_6); assert(piece_on(to) == NO_PIECE); assert(piece_on(capsq) == make_piece(them, PAWN)); - - board[capsq] = NO_PIECE; // Not done by remove_piece() } st->pawnKey ^= Zobrist::psq[captured][capsq]; @@ -752,8 +754,19 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { else st->nonPawnMaterial[them] -= PieceValue[MG][captured]; + if (Eval::useNNUE) + { + dp.dirty_num = 2; // 1 piece moved, 1 piece captured + dp.piece[1] = captured; + dp.from[1] = capsq; + dp.to[1] = SQ_NONE; + } + // Update board and piece lists - remove_piece(captured, capsq); + remove_piece(capsq); + + if (type_of(m) == EN_PASSANT) + board[capsq] = NO_PIECE; // Update material hash key and prefetch access to materialTable k ^= Zobrist::psq[captured][capsq]; @@ -777,21 +790,30 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Update castling rights if needed if (st->castlingRights && (castlingRightsMask[from] | castlingRightsMask[to])) { - int cr = castlingRightsMask[from] | castlingRightsMask[to]; - k ^= Zobrist::castling[st->castlingRights & cr]; - st->castlingRights &= ~cr; + k ^= Zobrist::castling[st->castlingRights]; + st->castlingRights &= ~(castlingRightsMask[from] | castlingRightsMask[to]); + k ^= Zobrist::castling[st->castlingRights]; } // Move the piece. The tricky Chess960 castling is handled earlier if (type_of(m) != CASTLING) - move_piece(pc, from, to); + { + if (Eval::useNNUE) + { + dp.piece[0] = pc; + dp.from[0] = from; + dp.to[0] = to; + } + + move_piece(from, to); + } // If the moving piece is a pawn do some special extra work if (type_of(pc) == PAWN) { - // Set en-passant square if the moved pawn can be captured + // Set en passant square if the moved pawn can be captured if ( (int(to) ^ int(from)) == 16 - && (attacks_from(to - pawn_push(us), us) & pieces(them, PAWN))) + && (pawn_attacks_bb(us, to - pawn_push(us)) & pieces(them, PAWN))) { st->epSquare = to - pawn_push(us); k ^= Zobrist::enpassant[file_of(st->epSquare)]; @@ -804,9 +826,19 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(relative_rank(us, to) == RANK_8); assert(type_of(promotion) >= KNIGHT && type_of(promotion) <= QUEEN); - remove_piece(pc, to); + remove_piece(to); put_piece(promotion, to); + if (Eval::useNNUE) + { + // Promoting pawn to SQ_NONE, promoted piece from SQ_NONE + dp.to[0] = SQ_NONE; + dp.piece[dp.dirty_num] = promotion; + dp.from[dp.dirty_num] = SQ_NONE; + dp.to[dp.dirty_num] = to; + dp.dirty_num++; + } + // Update hash keys k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to]; st->pawnKey ^= Zobrist::psq[pc][to]; @@ -884,7 +916,7 @@ void Position::undo_move(Move m) { assert(type_of(pc) == promotion_type(m)); assert(type_of(pc) >= KNIGHT && type_of(pc) <= QUEEN); - remove_piece(pc, to); + remove_piece(to); pc = make_piece(us, PAWN); put_piece(pc, to); } @@ -896,13 +928,13 @@ void Position::undo_move(Move m) { } else { - move_piece(pc, to, from); // Put the piece back at the source square + move_piece(to, from); // Put the piece back at the source square if (st->capturedPiece) { Square capsq = to; - if (type_of(m) == ENPASSANT) + if (type_of(m) == EN_PASSANT) { capsq -= pawn_push(us); @@ -935,16 +967,28 @@ void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Squ rto = relative_square(us, kingSide ? SQ_F1 : SQ_D1); to = relative_square(us, kingSide ? SQ_G1 : SQ_C1); + if (Do && Eval::useNNUE) + { + auto& dp = st->dirtyPiece; + dp.piece[0] = make_piece(us, KING); + dp.from[0] = from; + dp.to[0] = to; + dp.piece[1] = make_piece(us, ROOK); + dp.from[1] = rfrom; + dp.to[1] = rto; + dp.dirty_num = 2; + } + // Remove both pieces first since squares could overlap in Chess960 - remove_piece(make_piece(us, KING), Do ? from : to); - remove_piece(make_piece(us, ROOK), Do ? rfrom : rto); - board[Do ? from : to] = board[Do ? rfrom : rto] = NO_PIECE; // Since remove_piece doesn't do it for us + remove_piece(Do ? from : to); + remove_piece(Do ? rfrom : rto); + board[Do ? from : to] = board[Do ? rfrom : rto] = NO_PIECE; // Since remove_piece doesn't do this for us put_piece(make_piece(us, KING), Do ? to : from); put_piece(make_piece(us, ROOK), Do ? rto : rfrom); } -/// Position::do(undo)_null_move() is used to do(undo) a "null move": it flips +/// Position::do_null_move() is used to do a "null move": it flips /// the side to move without executing any move on the board. void Position::do_null_move(StateInfo& newSt) { @@ -952,10 +996,16 @@ void Position::do_null_move(StateInfo& newSt) { assert(!checkers()); assert(&newSt != st); - std::memcpy(&newSt, st, sizeof(StateInfo)); + std::memcpy(&newSt, st, offsetof(StateInfo, accumulator)); + newSt.previous = st; st = &newSt; + st->dirtyPiece.dirty_num = 0; + st->dirtyPiece.piece[0] = NO_PIECE; // Avoid checks in UpdateAccumulator() + st->accumulator.computed[WHITE] = false; + st->accumulator.computed[BLACK] = false; + if (st->epSquare != SQ_NONE) { st->key ^= Zobrist::enpassant[file_of(st->epSquare)]; @@ -963,9 +1013,9 @@ void Position::do_null_move(StateInfo& newSt) { } st->key ^= Zobrist::side; - prefetch(TT.first_entry(st->key)); - ++st->rule50; + prefetch(TT.first_entry(key())); + st->pliesFromNull = 0; sideToMove = ~sideToMove; @@ -977,6 +1027,9 @@ void Position::do_null_move(StateInfo& newSt) { assert(pos_is_ok()); } + +/// Position::undo_null_move() must be used to undo a "null move" + void Position::undo_null_move() { assert(!checkers()); @@ -988,7 +1041,7 @@ void Position::undo_null_move() { /// Position::key_after() computes the new hash key after the given move. Needed /// for speculative prefetch. It doesn't recognize special moves like castling, -/// en-passant and promotions. +/// en passant and promotions. Key Position::key_after(Move m) const { @@ -1013,7 +1066,7 @@ bool Position::see_ge(Move m, Value threshold) const { assert(is_ok(m)); - // Only deal with normal moves, assume others pass a simple see + // Only deal with normal moves, assume others pass a simple SEE if (type_of(m) != NORMAL) return VALUE_ZERO >= threshold; @@ -1027,8 +1080,9 @@ bool Position::see_ge(Move m, Value threshold) const { if (swap <= 0) return true; + assert(color_of(piece_on(from)) == sideToMove); Bitboard occupied = pieces() ^ from ^ to; - Color stm = color_of(piece_on(from)); + Color stm = sideToMove; Bitboard attackers = attackers_to(to, occupied); Bitboard stmAttackers, bb; int res = 1; @@ -1042,10 +1096,10 @@ bool Position::see_ge(Move m, Value threshold) const { if (!(stmAttackers = attackers & pieces(stm))) break; - // Don't allow pinned pieces to attack (except the king) as long as - // there are pinners on their original square. - if (st->pinners[~stm] & occupied) - stmAttackers &= ~st->blockersForKing[stm]; + // Don't allow pinned pieces to attack as long as there are + // pinners on their original square. + if (pinners(~stm) & occupied) + stmAttackers &= ~blockers_for_king(stm); if (!stmAttackers) break; @@ -1059,7 +1113,7 @@ bool Position::see_ge(Move m, Value threshold) const { if ((swap = PawnValueMg - swap) < res) break; - occupied ^= lsb(bb); + occupied ^= least_significant_square_bb(bb); attackers |= attacks_bb(to, occupied) & pieces(BISHOP, QUEEN); } @@ -1068,7 +1122,7 @@ bool Position::see_ge(Move m, Value threshold) const { if ((swap = KnightValueMg - swap) < res) break; - occupied ^= lsb(bb); + occupied ^= least_significant_square_bb(bb); } else if ((bb = stmAttackers & pieces(BISHOP))) @@ -1076,7 +1130,7 @@ bool Position::see_ge(Move m, Value threshold) const { if ((swap = BishopValueMg - swap) < res) break; - occupied ^= lsb(bb); + occupied ^= least_significant_square_bb(bb); attackers |= attacks_bb(to, occupied) & pieces(BISHOP, QUEEN); } @@ -1085,7 +1139,7 @@ bool Position::see_ge(Move m, Value threshold) const { if ((swap = RookValueMg - swap) < res) break; - occupied ^= lsb(bb); + occupied ^= least_significant_square_bb(bb); attackers |= attacks_bb(to, occupied) & pieces(ROOK, QUEEN); } @@ -1094,7 +1148,7 @@ bool Position::see_ge(Move m, Value threshold) const { if ((swap = QueenValueMg - swap) < res) break; - occupied ^= lsb(bb); + occupied ^= least_significant_square_bb(bb); attackers |= (attacks_bb(to, occupied) & pieces(BISHOP, QUEEN)) | (attacks_bb(to, occupied) & pieces(ROOK , QUEEN)); } @@ -1108,6 +1162,7 @@ bool Position::see_ge(Move m, Value threshold) const { return bool(res); } + /// Position::is_draw() tests whether the position is drawn by 50-move rule /// or by repetition. It does not detect stalemates. @@ -1118,10 +1173,7 @@ bool Position::is_draw(int ply) const { // Return a draw score if a position repeats once earlier but strictly // after the root, or repeats twice before or at the root. - if (st->repetition && st->repetition < ply) - return true; - - return false; + return st->repetition && st->repetition < ply; } @@ -1170,7 +1222,7 @@ bool Position::has_game_cycle(int ply) const { Square s1 = from_sq(move); Square s2 = to_sq(move); - if (!(between_bb(s1, s2) & pieces())) + if (!((between_bb(s1, s2) ^ s2) & pieces())) { if (ply > i) return true; @@ -1267,21 +1319,17 @@ bool Position::pos_is_ok() const { assert(0 && "pos_is_ok: Bitboards"); StateInfo si = *st; + ASSERT_ALIGNED(&si, Eval::NNUE::CacheLineSize); + set_state(&si); if (std::memcmp(&si, st, sizeof(StateInfo))) assert(0 && "pos_is_ok: State"); for (Piece pc : Pieces) - { if ( pieceCount[pc] != popcount(pieces(color_of(pc), type_of(pc))) || pieceCount[pc] != std::count(board, board + SQUARE_NB, pc)) assert(0 && "pos_is_ok: Pieces"); - for (int i = 0; i < pieceCount[pc]; ++i) - if (board[pieceList[pc][i]] != pc || index[pieceList[pc][i]] != i) - assert(0 && "pos_is_ok: Index"); - } - for (Color c : { WHITE, BLACK }) for (CastlingRights cr : {c & KING_SIDE, c & QUEEN_SIDE}) { @@ -1296,3 +1344,5 @@ bool Position::pos_is_ok() const { return true; } + +} // namespace Stockfish diff --git a/src/position.h b/src/position.h index 6791455fd00..8dbf1493c28 100644 --- a/src/position.h +++ b/src/position.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,8 +25,13 @@ #include #include "bitboard.h" +#include "evaluate.h" +#include "psqt.h" #include "types.h" +#include "nnue/nnue_accumulator.h" + +namespace Stockfish { /// StateInfo struct stores information needed to restore a Position object to /// its previous state when we retract a move. Whenever a move is made on the @@ -48,14 +51,19 @@ struct StateInfo { // Not copied when making a move (will be recomputed anyhow) Key key; Bitboard checkersBB; - Piece capturedPiece; StateInfo* previous; Bitboard blockersForKing[COLOR_NB]; Bitboard pinners[COLOR_NB]; Bitboard checkSquares[PIECE_TYPE_NB]; + Piece capturedPiece; int repetition; + + // Used by NNUE + Eval::NNUE::Accumulator accumulator; + DirtyPiece dirtyPiece; }; + /// A list to keep track of the position states along the setup moves (from the /// start position to the position just before the search starts). Needed by /// 'draw by repetition' detection. Use a std::deque because pointers to @@ -80,10 +88,9 @@ class Position { // FEN string input/output Position& set(const std::string& fenStr, bool isChess960, StateInfo* si, Thread* th); Position& set(const std::string& code, Color c, StateInfo* si); - const std::string fen() const; + std::string fen() const; // Position representation - Bitboard pieces() const; Bitboard pieces(PieceType pt) const; Bitboard pieces(PieceType pt1, PieceType pt2) const; Bitboard pieces(Color c) const; @@ -94,12 +101,11 @@ class Position { bool empty(Square s) const; template int count(Color c) const; template int count() const; - template const Square* squares(Color c) const; template Square square(Color c) const; bool is_on_semiopen_file(Color c, Square s) const; // Castling - int castling_rights(Color c) const; + CastlingRights castling_rights(Color c) const; bool can_castle(CastlingRights cr) const; bool castling_impeded(CastlingRights cr) const; Square castling_rook_square(CastlingRights cr) const; @@ -108,14 +114,11 @@ class Position { Bitboard checkers() const; Bitboard blockers_for_king(Color c) const; Bitboard check_squares(PieceType pt) const; - bool is_discovery_check_on_king(Color c, Move m) const; + Bitboard pinners(Color c) const; // Attacks to/from a given square Bitboard attackers_to(Square s) const; Bitboard attackers_to(Square s, Bitboard occupied) const; - Bitboard attacks_from(PieceType pt, Square s) const; - template Bitboard attacks_from(Square s) const; - template Bitboard attacks_from(Square s, Color c) const; Bitboard slider_blockers(Bitboard sliders, Square s, Bitboard& pinners) const; // Properties of moves @@ -124,7 +127,6 @@ class Position { bool capture(Move m) const; bool capture_or_promotion(Move m) const; bool gives_check(Move m) const; - bool advanced_pawn_push(Move m) const; Piece moved_piece(Move m) const; Piece captured_piece() const; @@ -166,6 +168,12 @@ class Position { bool pos_is_ok() const; void flip(); + // Used by NNUE + StateInfo* state() const; + + void put_piece(Piece pc, Square s); + void remove_piece(Square s); + private: // Initialization helpers (used while setting up a position) void set_castling_right(Color c, Square rfrom); @@ -173,9 +181,7 @@ class Position { void set_check_info(StateInfo* si) const; // Other helpers - void put_piece(Piece pc, Square s); - void remove_piece(Piece pc, Square s); - void move_piece(Piece pc, Square from, Square to); + void move_piece(Square from, Square to); template void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto); @@ -184,51 +190,42 @@ class Position { Bitboard byTypeBB[PIECE_TYPE_NB]; Bitboard byColorBB[COLOR_NB]; int pieceCount[PIECE_NB]; - Square pieceList[PIECE_NB][16]; - int index[SQUARE_NB]; int castlingRightsMask[SQUARE_NB]; Square castlingRookSquare[CASTLING_RIGHT_NB]; Bitboard castlingPath[CASTLING_RIGHT_NB]; + Thread* thisThread; + StateInfo* st; int gamePly; Color sideToMove; Score psq; - Thread* thisThread; - StateInfo* st; bool chess960; }; -namespace PSQT { - extern Score psq[PIECE_NB][SQUARE_NB]; -} - extern std::ostream& operator<<(std::ostream& os, const Position& pos); inline Color Position::side_to_move() const { return sideToMove; } -inline bool Position::empty(Square s) const { - return board[s] == NO_PIECE; -} - inline Piece Position::piece_on(Square s) const { + assert(is_ok(s)); return board[s]; } -inline Piece Position::moved_piece(Move m) const { - return board[from_sq(m)]; +inline bool Position::empty(Square s) const { + return piece_on(s) == NO_PIECE; } -inline Bitboard Position::pieces() const { - return byTypeBB[ALL_PIECES]; +inline Piece Position::moved_piece(Move m) const { + return piece_on(from_sq(m)); } -inline Bitboard Position::pieces(PieceType pt) const { +inline Bitboard Position::pieces(PieceType pt = ALL_PIECES) const { return byTypeBB[pt]; } inline Bitboard Position::pieces(PieceType pt1, PieceType pt2) const { - return byTypeBB[pt1] | byTypeBB[pt2]; + return pieces(pt1) | pieces(pt2); } inline Bitboard Position::pieces(Color c) const { @@ -236,11 +233,11 @@ inline Bitboard Position::pieces(Color c) const { } inline Bitboard Position::pieces(Color c, PieceType pt) const { - return byColorBB[c] & byTypeBB[pt]; + return pieces(c) & pieces(pt); } inline Bitboard Position::pieces(Color c, PieceType pt1, PieceType pt2) const { - return byColorBB[c] & (byTypeBB[pt1] | byTypeBB[pt2]); + return pieces(c) & (pieces(pt1) | pieces(pt2)); } template inline int Position::count(Color c) const { @@ -248,16 +245,12 @@ template inline int Position::count(Color c) const { } template inline int Position::count() const { - return pieceCount[make_piece(WHITE, Pt)] + pieceCount[make_piece(BLACK, Pt)]; -} - -template inline const Square* Position::squares(Color c) const { - return pieceList[make_piece(c, Pt)]; + return count(WHITE) + count(BLACK); } template inline Square Position::square(Color c) const { - assert(pieceCount[make_piece(c, Pt)] == 1); - return pieceList[make_piece(c, Pt)][0]; + assert(count(c) == 1); + return lsb(pieces(c, Pt)); } inline Square Position::ep_square() const { @@ -272,14 +265,14 @@ inline bool Position::can_castle(CastlingRights cr) const { return st->castlingRights & cr; } -inline int Position::castling_rights(Color c) const { - return st->castlingRights & (c == WHITE ? WHITE_CASTLING : BLACK_CASTLING); +inline CastlingRights Position::castling_rights(Color c) const { + return c & CastlingRights(st->castlingRights); } inline bool Position::castling_impeded(CastlingRights cr) const { assert(cr == WHITE_OO || cr == WHITE_OOO || cr == BLACK_OO || cr == BLACK_OOO); - return byTypeBB[ALL_PIECES] & castlingPath[cr]; + return pieces() & castlingPath[cr]; } inline Square Position::castling_rook_square(CastlingRights cr) const { @@ -288,26 +281,8 @@ inline Square Position::castling_rook_square(CastlingRights cr) const { return castlingRookSquare[cr]; } -template -inline Bitboard Position::attacks_from(Square s) const { - static_assert(Pt != PAWN, "Pawn attacks need color"); - - return Pt == BISHOP || Pt == ROOK ? attacks_bb(s, byTypeBB[ALL_PIECES]) - : Pt == QUEEN ? attacks_from(s) | attacks_from(s) - : PseudoAttacks[Pt][s]; -} - -template<> -inline Bitboard Position::attacks_from(Square s, Color c) const { - return PawnAttacks[c][s]; -} - -inline Bitboard Position::attacks_from(PieceType pt, Square s) const { - return attacks_bb(pt, s, byTypeBB[ALL_PIECES]); -} - inline Bitboard Position::attackers_to(Square s) const { - return attackers_to(s, byTypeBB[ALL_PIECES]); + return attackers_to(s, pieces()); } inline Bitboard Position::checkers() const { @@ -318,29 +293,25 @@ inline Bitboard Position::blockers_for_king(Color c) const { return st->blockersForKing[c]; } -inline Bitboard Position::check_squares(PieceType pt) const { - return st->checkSquares[pt]; +inline Bitboard Position::pinners(Color c) const { + return st->pinners[c]; } -inline bool Position::is_discovery_check_on_king(Color c, Move m) const { - return st->blockersForKing[c] & from_sq(m); +inline Bitboard Position::check_squares(PieceType pt) const { + return st->checkSquares[pt]; } inline bool Position::pawn_passed(Color c, Square s) const { return !(pieces(~c, PAWN) & passed_pawn_span(c, s)); } -inline bool Position::advanced_pawn_push(Move m) const { - return type_of(moved_piece(m)) == PAWN - && relative_rank(sideToMove, to_sq(m)) > RANK_5; -} - inline int Position::pawns_on_same_color_squares(Color c, Square s) const { return popcount(pieces(c, PAWN) & ((DarkSquares & s) ? DarkSquares : ~DarkSquares)); } inline Key Position::key() const { - return st->key; + return st->rule50 < 14 ? st->key + : st->key ^ make_key((st->rule50 - 14) / 8); } inline Key Position::pawn_key() const { @@ -360,7 +331,7 @@ inline Value Position::non_pawn_material(Color c) const { } inline Value Position::non_pawn_material() const { - return st->nonPawnMaterial[WHITE] + st->nonPawnMaterial[BLACK]; + return non_pawn_material(WHITE) + non_pawn_material(BLACK); } inline int Position::game_ply() const { @@ -372,8 +343,8 @@ inline int Position::rule50_count() const { } inline bool Position::opposite_bishops() const { - return pieceCount[W_BISHOP] == 1 - && pieceCount[B_BISHOP] == 1 + return count(WHITE) == 1 + && count(BLACK) == 1 && opposite_colors(square(WHITE), square(BLACK)); } @@ -389,7 +360,7 @@ inline bool Position::capture_or_promotion(Move m) const { inline bool Position::capture(Move m) const { assert(is_ok(m)); // Castling is encoded as "king captures rook" - return (!empty(to_sq(m)) && type_of(m) != CASTLING) || type_of(m) == ENPASSANT; + return (!empty(to_sq(m)) && type_of(m) != CASTLING) || type_of(m) == EN_PASSANT; } inline Piece Position::captured_piece() const { @@ -403,45 +374,34 @@ inline Thread* Position::this_thread() const { inline void Position::put_piece(Piece pc, Square s) { board[s] = pc; - byTypeBB[ALL_PIECES] |= s; - byTypeBB[type_of(pc)] |= s; + byTypeBB[ALL_PIECES] |= byTypeBB[type_of(pc)] |= s; byColorBB[color_of(pc)] |= s; - index[s] = pieceCount[pc]++; - pieceList[pc][index[s]] = s; + pieceCount[pc]++; pieceCount[make_piece(color_of(pc), ALL_PIECES)]++; psq += PSQT::psq[pc][s]; } -inline void Position::remove_piece(Piece pc, Square s) { +inline void Position::remove_piece(Square s) { - // WARNING: This is not a reversible operation. If we remove a piece in - // do_move() and then replace it in undo_move() we will put it at the end of - // the list and not in its original place, it means index[] and pieceList[] - // are not invariant to a do_move() + undo_move() sequence. + Piece pc = board[s]; byTypeBB[ALL_PIECES] ^= s; byTypeBB[type_of(pc)] ^= s; byColorBB[color_of(pc)] ^= s; - /* board[s] = NO_PIECE; Not needed, overwritten by the capturing one */ - Square lastSquare = pieceList[pc][--pieceCount[pc]]; - index[lastSquare] = index[s]; - pieceList[pc][index[lastSquare]] = lastSquare; - pieceList[pc][pieceCount[pc]] = SQ_NONE; + board[s] = NO_PIECE; + pieceCount[pc]--; pieceCount[make_piece(color_of(pc), ALL_PIECES)]--; psq -= PSQT::psq[pc][s]; } -inline void Position::move_piece(Piece pc, Square from, Square to) { +inline void Position::move_piece(Square from, Square to) { - // index[from] is not updated and becomes stale. This works as long as index[] - // is accessed just by known occupied squares. + Piece pc = board[from]; Bitboard fromTo = from | to; byTypeBB[ALL_PIECES] ^= fromTo; byTypeBB[type_of(pc)] ^= fromTo; byColorBB[color_of(pc)] ^= fromTo; board[from] = NO_PIECE; board[to] = pc; - index[to] = index[from]; - pieceList[pc][index[to]] = to; psq += PSQT::psq[pc][to] - PSQT::psq[pc][from]; } @@ -449,4 +409,11 @@ inline void Position::do_move(Move m, StateInfo& newSt) { do_move(m, newSt, gives_check(m)); } +inline StateInfo* Position::state() const { + + return st; +} + +} // namespace Stockfish + #endif // #ifndef POSITION_H_INCLUDED diff --git a/src/psqt.cpp b/src/psqt.cpp index 647bd8642df..ca5664c259f 100644 --- a/src/psqt.cpp +++ b/src/psqt.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,23 +16,23 @@ along with this program. If not, see . */ + +#include "psqt.h" + #include +#include "bitboard.h" #include "types.h" -Value PieceValue[PHASE_NB][PIECE_NB] = { - { VALUE_ZERO, PawnValueMg, KnightValueMg, BishopValueMg, RookValueMg, QueenValueMg }, - { VALUE_ZERO, PawnValueEg, KnightValueEg, BishopValueEg, RookValueEg, QueenValueEg } -}; +namespace Stockfish { -namespace PSQT { +namespace +{ -#define S(mg, eg) make_score(mg, eg) +auto constexpr S = make_score; -// Bonus[PieceType][Square / 2] contains Piece-Square scores. For each piece -// type on a given square a (middlegame, endgame) score pair is assigned. Table -// is defined for files A..D and white side: it is symmetric for black side and -// second half of the files. +// 'Bonus' contains Piece-Square parameters. +// Scores are explicit for files A to D, implicitly mirrored for E to H. constexpr Score Bonus[][RANK_NB][int(FILE_NB) / 2] = { { }, { }, @@ -49,14 +47,14 @@ constexpr Score Bonus[][RANK_NB][int(FILE_NB) / 2] = { { S(-201,-100), S(-83,-88), S(-56,-56), S(-26,-17) } }, { // Bishop - { S(-53,-57), S( -5,-30), S( -8,-37), S(-23,-12) }, - { S(-15,-37), S( 8,-13), S( 19,-17), S( 4, 1) }, - { S( -7,-16), S( 21, -1), S( -5, -2), S( 17, 10) }, - { S( -5,-20), S( 11, -6), S( 25, 0), S( 39, 17) }, - { S(-12,-17), S( 29, -1), S( 22,-14), S( 31, 15) }, - { S(-16,-30), S( 6, 6), S( 1, 4), S( 11, 6) }, - { S(-17,-31), S(-14,-20), S( 5, -1), S( 0, 1) }, - { S(-48,-46), S( 1,-42), S(-14,-37), S(-23,-24) } + { S(-37,-40), S(-4 ,-21), S( -6,-26), S(-16, -8) }, + { S(-11,-26), S( 6, -9), S( 13,-12), S( 3, 1) }, + { S(-5 ,-11), S( 15, -1), S( -4, -1), S( 12, 7) }, + { S(-4 ,-14), S( 8, -4), S( 18, 0), S( 27, 12) }, + { S(-8 ,-12), S( 20, -1), S( 15,-10), S( 22, 11) }, + { S(-11,-21), S( 4, 4), S( 1, 3), S( 8, 4) }, + { S(-12,-22), S(-10,-14), S( 4, -1), S( 0, 1) }, + { S(-34,-32), S( 1,-29), S(-10,-26), S(-16,-17) } }, { // Rook { S(-31, -9), S(-20,-13), S(-14,-10), S(-5, -9) }, @@ -70,13 +68,13 @@ constexpr Score Bonus[][RANK_NB][int(FILE_NB) / 2] = { }, { // Queen { S( 3,-69), S(-5,-57), S(-5,-47), S( 4,-26) }, - { S(-3,-55), S( 5,-31), S( 8,-22), S(12, -4) }, + { S(-3,-54), S( 5,-31), S( 8,-22), S(12, -4) }, { S(-3,-39), S( 6,-18), S(13, -9), S( 7, 3) }, { S( 4,-23), S( 5, -3), S( 9, 13), S( 8, 24) }, { S( 0,-29), S(14, -6), S(12, 9), S( 5, 21) }, - { S(-4,-38), S(10,-18), S( 6,-12), S( 8, 1) }, + { S(-4,-38), S(10,-18), S( 6,-11), S( 8, 1) }, { S(-5,-50), S( 6,-27), S(10,-24), S( 8, -8) }, - { S(-2,-75), S(-2,-52), S( 1,-43), S(-2,-36) } + { S(-2,-74), S(-2,-52), S( 1,-43), S(-2,-34) } }, { // King { S(271, 1), S(327, 45), S(271, 85), S(198, 76) }, @@ -93,38 +91,41 @@ constexpr Score Bonus[][RANK_NB][int(FILE_NB) / 2] = { constexpr Score PBonus[RANK_NB][FILE_NB] = { // Pawn (asymmetric distribution) { }, - { S( 3,-10), S( 3, -6), S( 10, 10), S( 19, 0), S( 16, 14), S( 19, 7), S( 7, -5), S( -5,-19) }, - { S( -9,-10), S(-15,-10), S( 11,-10), S( 15, 4), S( 32, 4), S( 22, 3), S( 5, -6), S(-22, -4) }, - { S( -8, 6), S(-23, -2), S( 6, -8), S( 20, -4), S( 40,-13), S( 17,-12), S( 4,-10), S(-12, -9) }, - { S( 13, 9), S( 0, 4), S(-13, 3), S( 1,-12), S( 11,-12), S( -2, -6), S(-13, 13), S( 5, 8) }, - { S( -5, 28), S(-12, 20), S( -7, 21), S( 22, 28), S( -8, 30), S( -5, 7), S(-15, 6), S(-18, 13) }, - { S( -7, 0), S( 7,-11), S( -3, 12), S(-13, 21), S( 5, 25), S(-16, 19), S( 10, 4), S( -8, 7) } + { S( 2, -8), S( 4, -6), S( 11, 9), S( 18, 5), S( 16, 16), S( 21, 6), S( 9, -6), S( -3,-18) }, + { S( -9, -9), S(-15, -7), S( 11,-10), S( 15, 5), S( 31, 2), S( 23, 3), S( 6, -8), S(-20, -5) }, + { S( -3, 7), S(-20, 1), S( 8, -8), S( 19, -2), S( 39,-14), S( 17,-13), S( 2,-11), S( -5, -6) }, + { S( 11, 12), S( -4, 6), S(-11, 2), S( 2, -6), S( 11, -5), S( 0, -4), S(-12, 14), S( 5, 9) }, + { S( 3, 27), S(-11, 18), S( -6, 19), S( 22, 29), S( -8, 30), S( -5, 9), S(-14, 8), S(-11, 14) }, + { S( -7, -1), S( 6,-14), S( -2, 13), S(-11, 22), S( 4, 24), S(-14, 17), S( 10, 7), S( -9, 7) } }; -#undef S +} // namespace + + +namespace PSQT +{ Score psq[PIECE_NB][SQUARE_NB]; -// init() initializes piece-square tables: the white halves of the tables are -// copied from Bonus[] adding the piece value, then the black halves of the -// tables are initialized by flipping and changing the sign of the white scores. +// PSQT::init() initializes piece-square tables: the white halves of the tables are +// copied from Bonus[] and PBonus[], adding the piece value, then the black halves of +// the tables are initialized by flipping and changing the sign of the white scores. void init() { - for (Piece pc = W_PAWN; pc <= W_KING; ++pc) + for (Piece pc : {W_PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING}) { - PieceValue[MG][~pc] = PieceValue[MG][pc]; - PieceValue[EG][~pc] = PieceValue[EG][pc]; - - Score score = make_score(PieceValue[MG][pc], PieceValue[EG][pc]); - - for (Square s = SQ_A1; s <= SQ_H8; ++s) - { - File f = map_to_queenside(file_of(s)); - psq[ pc][ s] = score + (type_of(pc) == PAWN ? PBonus[rank_of(s)][file_of(s)] - : Bonus[pc][rank_of(s)][f]); - psq[~pc][~s] = -psq[pc][s]; - } + Score score = make_score(PieceValue[MG][pc], PieceValue[EG][pc]); + + for (Square s = SQ_A1; s <= SQ_H8; ++s) + { + File f = File(edge_distance(file_of(s))); + psq[ pc][s] = score + (type_of(pc) == PAWN ? PBonus[rank_of(s)][file_of(s)] + : Bonus[pc][rank_of(s)][f]); + psq[~pc][flip_rank(s)] = -psq[pc][s]; + } } } } // namespace PSQT + +} // namespace Stockfish diff --git a/src/psqt.h b/src/psqt.h new file mode 100644 index 00000000000..4ee0e379b14 --- /dev/null +++ b/src/psqt.h @@ -0,0 +1,38 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef PSQT_H_INCLUDED +#define PSQT_H_INCLUDED + + +#include "types.h" + + +namespace Stockfish::PSQT +{ + +extern Score psq[PIECE_NB][SQUARE_NB]; + +// Fill psqt array from a set of internally linked parameters +extern void init(); + +} // namespace Stockfish::PSQT + + +#endif // PSQT_H_INCLUDED diff --git a/src/search.cpp b/src/search.cpp index 0eea4127f80..c9d5da64c2f 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,6 +35,8 @@ #include "uci.h" #include "syzygy/tbprobe.h" +namespace Stockfish { + namespace Search { LimitsType Limits; @@ -59,97 +59,82 @@ using namespace Search; namespace { // Different node types, used as a template parameter - enum NodeType { NonPV, PV }; - - constexpr uint64_t ttHitAverageWindow = 4096; - constexpr uint64_t ttHitAverageResolution = 1024; + enum NodeType { NonPV, PV, Root }; - // Razor and futility margins - constexpr int RazorMargin = 531; + // Futility margin Value futility_margin(Depth d, bool improving) { - return Value(217 * (d - improving)); + return Value(214 * (d - improving)); } // Reductions lookup table, initialized at startup int Reductions[MAX_MOVES]; // [depth or moveNumber] - Depth reduction(bool i, Depth d, int mn) { + Depth reduction(bool i, Depth d, int mn, Value delta, Value rootDelta) { int r = Reductions[d] * Reductions[mn]; - return (r + 511) / 1024 + (!i && r > 1007); + return (r + 1358 - int(delta) * 1024 / int(rootDelta)) / 1024 + (!i && r > 904); } constexpr int futility_move_count(bool improving, Depth depth) { - return (5 + depth * depth) * (1 + improving) / 2 - 1; + return (3 + depth * depth) / (2 - improving); } // History and stats update bonus, based on depth int stat_bonus(Depth d) { - return d > 15 ? -8 : 19 * d * d + 155 * d - 132; + return std::min((6 * d + 229) * d - 215 , 2000); } - // Add a small random component to draw evaluations to avoid 3fold-blindness + // Add a small random component to draw evaluations to avoid 3-fold blindness Value value_draw(Thread* thisThread) { return VALUE_DRAW + Value(2 * (thisThread->nodes & 1) - 1); } - // Skill structure is used to implement strength limit - struct Skill { - explicit Skill(int l) : level(l) {} - bool enabled() const { return level < 20; } - bool time_to_pick(Depth depth) const { return depth == 1 + level; } - Move pick_best(size_t multiPV); + // Check if the current thread is in a search explosion + ExplosionState search_explosion(Thread* thisThread) { - int level; - Move best = MOVE_NONE; - }; + uint64_t nodesNow = thisThread->nodes; + bool explosive = thisThread->doubleExtensionAverage[WHITE].is_greater(2, 100) + || thisThread->doubleExtensionAverage[BLACK].is_greater(2, 100); - // Breadcrumbs are used to mark nodes as being searched by a given thread - struct Breadcrumb { - std::atomic thread; - std::atomic key; - }; - std::array breadcrumbs; - - // ThreadHolding structure keeps track of which thread left breadcrumbs at the given - // node for potential reductions. A free node will be marked upon entering the moves - // loop by the constructor, and unmarked upon leaving that loop by the destructor. - struct ThreadHolding { - explicit ThreadHolding(Thread* thisThread, Key posKey, int ply) { - location = ply < 8 ? &breadcrumbs[posKey & (breadcrumbs.size() - 1)] : nullptr; - otherThread = false; - owning = false; - if (location) - { - // See if another already marked this location, if not, mark it ourselves - Thread* tmp = (*location).thread.load(std::memory_order_relaxed); - if (tmp == nullptr) - { - (*location).thread.store(thisThread, std::memory_order_relaxed); - (*location).key.store(posKey, std::memory_order_relaxed); - owning = true; - } - else if ( tmp != thisThread - && (*location).key.load(std::memory_order_relaxed) == posKey) - otherThread = true; - } - } + if (explosive) + thisThread->nodesLastExplosive = nodesNow; + else + thisThread->nodesLastNormal = nodesNow; - ~ThreadHolding() { - if (owning) // Free the marked location - (*location).thread.store(nullptr, std::memory_order_relaxed); - } + if ( explosive + && thisThread->state == EXPLOSION_NONE + && nodesNow - thisThread->nodesLastNormal > 6000000) + thisThread->state = MUST_CALM_DOWN; + + if ( thisThread->state == MUST_CALM_DOWN + && nodesNow - thisThread->nodesLastExplosive > 6000000) + thisThread->state = EXPLOSION_NONE; + + return thisThread->state; + } - bool marked() { return otherThread; } + // Skill structure is used to implement strength limit. If we have an uci_elo then + // we convert it to a suitable fractional skill level using anchoring to CCRL Elo + // (goldfish 1.13 = 2000) and a fit through Ordo derived Elo for match (TC 60+0.6) + // results spanning a wide range of k values. + struct Skill { + Skill(int skill_level, int uci_elo) { + if (uci_elo) + level = std::clamp(std::pow((uci_elo - 1346.6) / 143.4, 1 / 0.806), 0.0, 20.0); + else + level = double(skill_level); + } + bool enabled() const { return level < 20.0; } + bool time_to_pick(Depth depth) const { return depth == 1 + int(level); } + Move pick_best(size_t multiPV); - private: - Breadcrumb* location; - bool otherThread, owning; + double level; + Move best = MOVE_NONE; }; - template + template Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode); - template + template Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0); Value value_to_tt(Value v, int ply); @@ -166,6 +151,8 @@ namespace { uint64_t perft(Position& pos, Depth depth) { StateInfo st; + ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); + uint64_t cnt, nodes = 0; const bool leaf = (depth == 2); @@ -194,7 +181,7 @@ namespace { void Search::init() { for (int i = 1; i < MAX_MOVES; ++i) - Reductions[i] = int((24.8 + std::log(Threads.size()) / 2) * std::log(i)); + Reductions[i] = int((21.9 + std::log(Threads.size()) / 2) * std::log(i)); } @@ -227,6 +214,8 @@ void MainThread::search() { Time.init(Limits, us, rootPos.game_ply()); TT.new_search(); + Eval::NNUE::verify(); + if (rootMoves.empty()) { rootMoves.emplace_back(MOVE_NONE); @@ -236,14 +225,8 @@ void MainThread::search() { } else { - for (Thread* th : Threads) - { - th->bestMoveChanges = 0; - if (th != this) - th->start_searching(); - } - - Thread::search(); // Let's start searching! + Threads.start_searching(); // start non-main threads + Thread::search(); // main thread start searching } // When we reach the maximum depth, we can arrive here without a raise of @@ -260,9 +243,7 @@ void MainThread::search() { Threads.stop = true; // Wait until all threads have finished - for (Thread* th : Threads) - if (th != this) - th->wait_for_search_finished(); + Threads.wait_for_search_finished(); // When playing in 'nodes as time' mode, subtract the searched nodes from // the available ones before exiting. @@ -270,39 +251,16 @@ void MainThread::search() { Time.availableNodes += Limits.inc[us] - Threads.nodes_searched(); Thread* bestThread = this; + Skill skill = Skill(Options["Skill Level"], Options["UCI_LimitStrength"] ? int(Options["UCI_Elo"]) : 0); - // Check if there are threads with a better score than main thread - if ( Options["MultiPV"] == 1 + if ( int(Options["MultiPV"]) == 1 && !Limits.depth - && !(Skill(Options["Skill Level"]).enabled() || Options["UCI_LimitStrength"]) - && rootMoves[0].pv[0] != MOVE_NONE) - { - std::map votes; - Value minScore = this->rootMoves[0].score; - - // Find out minimum score - for (Thread* th: Threads) - minScore = std::min(minScore, th->rootMoves[0].score); - - // Vote according to score and depth, and select the best thread - for (Thread* th : Threads) - { - votes[th->rootMoves[0].pv[0]] += - (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth); - - if (bestThread->rootMoves[0].score >= VALUE_MATE_IN_MAX_PLY) - { - // Make sure we pick the shortest mate - if (th->rootMoves[0].score > bestThread->rootMoves[0].score) - bestThread = th; - } - else if ( th->rootMoves[0].score >= VALUE_MATE_IN_MAX_PLY - || votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]]) - bestThread = th; - } - } + && !skill.enabled() + && rootMoves[0].pv[0] != MOVE_NONE) + bestThread = Threads.get_best_thread(); - previousScore = bestThread->rootMoves[0].score; + bestPreviousScore = bestThread->rootMoves[0].score; + bestPreviousAverageScore = bestThread->rootMoves[0].averageScore; // Send again PV info if we have a new best thread if (bestThread != this) @@ -326,10 +284,10 @@ void Thread::search() { // To allow access to (ss-7) up to (ss+2), the stack must be oversized. // The former is needed to allow update_continuation_histories(ss-1, ...), // which accesses its argument at ss-6, also near the root. - // The latter is needed for statScores and killer initialization. + // The latter is needed for statScore and killer initialization. Stack stack[MAX_PLY+10], *ss = stack+7; Move pv[MAX_PLY+1]; - Value bestValue, alpha, beta, delta; + Value alpha, beta, delta; Move lastBestMove = MOVE_NONE; Depth lastBestMoveDepth = 0; MainThread* mainThread = (this == Threads.main() ? Threads.main() : nullptr); @@ -341,6 +299,9 @@ void Thread::search() { for (int i = 7; i > 0; i--) (ss-i)->continuationHistory = &this->continuationHistory[0][0][NO_PIECE][0]; // Use as a sentinel + for (int i = 0; i <= MAX_PLY + 2; ++i) + (ss+i)->ply = i; + ss->pv = pv; bestValue = delta = alpha = -VALUE_INFINITE; @@ -348,28 +309,16 @@ void Thread::search() { if (mainThread) { - if (mainThread->previousScore == VALUE_INFINITE) - for (int i=0; i<4; ++i) + if (mainThread->bestPreviousScore == VALUE_INFINITE) + for (int i = 0; i < 4; ++i) mainThread->iterValue[i] = VALUE_ZERO; else - for (int i=0; i<4; ++i) - mainThread->iterValue[i] = mainThread->previousScore; + for (int i = 0; i < 4; ++i) + mainThread->iterValue[i] = mainThread->bestPreviousScore; } - size_t multiPV = Options["MultiPV"]; - - // Pick integer skill levels, but non-deterministically round up or down - // such that the average integer skill corresponds to the input floating point one. - // UCI_Elo is converted to a suitable fractional skill level, using anchoring - // to CCRL Elo (goldfish 1.13 = 2000) and a fit through Ordo derived Elo - // for match (TC 60+0.6) results spanning a wide range of k values. - PRNG rng(now()); - double floatLevel = Options["UCI_LimitStrength"] ? - clamp(std::pow((Options["UCI_Elo"] - 1346.6) / 143.4, 1 / 0.806), 0.0, 20.0) : - double(Options["Skill Level"]); - int intLevel = int(floatLevel) + - ((floatLevel - int(floatLevel)) * 1024 > rng.rand() % 1024 ? 1 : 0); - Skill skill(intLevel); + size_t multiPV = size_t(Options["MultiPV"]); + Skill skill(Options["Skill Level"], Options["UCI_LimitStrength"] ? int(Options["UCI_Elo"]) : 0); // When playing with strength handicap enable MultiPV search that we will // use behind the scenes to retrieve a set of possible moves. @@ -377,21 +326,17 @@ void Thread::search() { multiPV = std::max(multiPV, (size_t)4); multiPV = std::min(multiPV, rootMoves.size()); - ttHitAverage = ttHitAverageWindow * ttHitAverageResolution / 2; - int ct = int(Options["Contempt"]) * PawnValueEg / 100; // From centipawns + doubleExtensionAverage[WHITE].set(0, 100); // initialize the running average at 0% + doubleExtensionAverage[BLACK].set(0, 100); // initialize the running average at 0% + complexityAverage.set(232, 1); - // In analysis mode, adjust contempt in accordance with user preference - if (Limits.infinite || Options["UCI_AnalyseMode"]) - ct = Options["Analysis Contempt"] == "Off" ? 0 - : Options["Analysis Contempt"] == "Both" ? ct - : Options["Analysis Contempt"] == "White" && us == BLACK ? -ct - : Options["Analysis Contempt"] == "Black" && us == WHITE ? -ct - : ct; - - // Evaluation score is from the white point of view - contempt = (us == WHITE ? make_score(ct, ct / 2) - : -make_score(ct, ct / 2)); + nodesLastExplosive = nodes; + nodesLastNormal = nodes; + state = EXPLOSION_NONE; + trend = SCORE_ZERO; + optimism[ us] = Value(25); + optimism[~us] = -optimism[us]; int searchAgainCounter = 0; @@ -432,16 +377,19 @@ void Thread::search() { // Reset aspiration window starting size if (rootDepth >= 4) { - Value previousScore = rootMoves[pvIdx].previousScore; - delta = Value(21 + abs(previousScore) / 256); - alpha = std::max(previousScore - delta,-VALUE_INFINITE); - beta = std::min(previousScore + delta, VALUE_INFINITE); - - // Adjust contempt based on root move's previousScore (dynamic contempt) - int dct = ct + (102 - ct / 2) * previousScore / (abs(previousScore) + 157); - - contempt = (us == WHITE ? make_score(dct, dct / 2) - : -make_score(dct, dct / 2)); + Value prev = rootMoves[pvIdx].averageScore; + delta = Value(17) + int(prev) * prev / 16384; + alpha = std::max(prev - delta,-VALUE_INFINITE); + beta = std::min(prev + delta, VALUE_INFINITE); + + // Adjust trend and optimism based on root move's previousScore + int tr = sigmoid(prev, 0, 0, 147, 113, 1); + trend = (us == WHITE ? make_score(tr, tr / 2) + : -make_score(tr, tr / 2)); + + int opt = sigmoid(prev, 0, 25, 147, 14464, 256); + optimism[ us] = Value(opt); + optimism[~us] = -optimism[us]; } // Start with a small aspiration window and, in the case of a fail @@ -451,7 +399,7 @@ void Thread::search() { while (true) { Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt - searchAgainCounter); - bestValue = ::search(rootPos, ss, alpha, beta, adjustedDepth, false); + bestValue = Stockfish::search(rootPos, ss, alpha, beta, adjustedDepth, false); // Bring the best move to the front. It is critical that sorting // is done with a stable algorithm because all the values but the @@ -492,10 +440,7 @@ void Thread::search() { ++failedHighCnt; } else - { - ++rootMoves[pvIdx].bestMoveCount; break; - } delta += delta / 4 + 5; @@ -531,30 +476,39 @@ void Thread::search() { if (skill.enabled() && skill.time_to_pick(rootDepth)) skill.pick_best(multiPV); + // Use part of the gained time from a previous stable move for the current move + for (Thread* th : Threads) + { + totBestMoveChanges += th->bestMoveChanges; + th->bestMoveChanges = 0; + } + // Do we have time for the next iteration? Can we stop searching now? if ( Limits.use_time_management() && !Threads.stop && !mainThread->stopOnPonderhit) { - double fallingEval = (332 + 6 * (mainThread->previousScore - bestValue) - + 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 704.0; - fallingEval = clamp(fallingEval, 0.5, 1.5); + double fallingEval = (142 + 12 * (mainThread->bestPreviousAverageScore - bestValue) + + 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 825.0; + fallingEval = std::clamp(fallingEval, 0.5, 1.5); // If the bestMove is stable over several iterations, reduce time accordingly - timeReduction = lastBestMoveDepth + 9 < completedDepth ? 1.94 : 0.91; - double reduction = (1.41 + mainThread->previousTimeReduction) / (2.27 * timeReduction); - - // Use part of the gained time from a previous stable move for the current move - for (Thread* th : Threads) - { - totBestMoveChanges += th->bestMoveChanges; - th->bestMoveChanges = 0; - } - double bestMoveInstability = 1 + totBestMoveChanges / Threads.size(); - - // Stop the search if we have only one legal move, or if available time elapsed - if ( rootMoves.size() == 1 - || Time.elapsed() > Time.optimum() * fallingEval * reduction * bestMoveInstability) + timeReduction = lastBestMoveDepth + 9 < completedDepth ? 1.92 : 0.95; + double reduction = (1.47 + mainThread->previousTimeReduction) / (2.32 * timeReduction); + double bestMoveInstability = 1.073 + std::max(1.0, 2.25 - 9.9 / rootDepth) + * totBestMoveChanges / Threads.size(); + int complexity = mainThread->complexityAverage.value(); + double complexPosition = std::clamp(1.0 + (complexity - 232) / 1750.0, 0.5, 1.5); + + double totalTime = Time.optimum() * fallingEval * reduction * bestMoveInstability * complexPosition; + + // Cap used time in case of a single legal move for a better viewer experience in tournaments + // yielding correct scores and sufficiently fast moves. + if (rootMoves.size() == 1) + totalTime = std::min(500.0, totalTime); + + // Stop the search if we have exceeded the totalTime + if (Time.elapsed() > totalTime) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". @@ -565,7 +519,7 @@ void Thread::search() { } else if ( Threads.increaseDepth && !mainThread->ponder - && Time.elapsed() > Time.optimum() * fallingEval * reduction * bestMoveInstability * 0.6) + && Time.elapsed() > totalTime * 0.58) Threads.increaseDepth = false; else Threads.increaseDepth = true; @@ -591,17 +545,26 @@ namespace { // search<>() is the main search function for both PV and non-PV nodes - template + template Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode) { - constexpr bool PvNode = NT == PV; - const bool rootNode = PvNode && ss->ply == 0; + Thread* thisThread = pos.this_thread(); + + // Step 0. Limit search explosion + if ( ss->ply > 10 + && search_explosion(thisThread) == MUST_CALM_DOWN + && depth > (ss-1)->depth) + depth = (ss-1)->depth; + + constexpr bool PvNode = nodeType != NonPV; + constexpr bool rootNode = nodeType == Root; + const Depth maxNextDepth = rootNode ? depth : depth + 1; // Check if we have an upcoming move which draws by repetition, or // if the opponent had an alternative move earlier to this position. - if ( pos.rule50_count() >= 3 + if ( !rootNode + && pos.rule50_count() >= 3 && alpha < VALUE_DRAW - && !rootNode && pos.has_game_cycle(ss->ply)) { alpha = value_draw(pos.this_thread()); @@ -611,7 +574,7 @@ namespace { // Dive into quiescence search when the depth reaches zero if (depth <= 0) - return qsearch(pos, ss, alpha, beta); + return qsearch(pos, ss, alpha, beta); assert(-VALUE_INFINITE <= alpha && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); @@ -620,24 +583,25 @@ namespace { Move pv[MAX_PLY+1], capturesSearched[32], quietsSearched[64]; StateInfo st; + ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); + TTEntry* tte; Key posKey; Move ttMove, move, excludedMove, bestMove; Depth extension, newDepth; - Value bestValue, value, ttValue, eval, maxValue; - bool ttHit, ttPv, inCheck, givesCheck, improving, didLMR, priorCapture; - bool captureOrPromotion, doFullDepthSearch, moveCountPruning, ttCapture, singularLMR; + Value bestValue, value, ttValue, eval, maxValue, probCutBeta; + bool givesCheck, improving, didLMR, priorCapture; + bool captureOrPromotion, doFullDepthSearch, moveCountPruning, ttCapture; Piece movedPiece; - int moveCount, captureCount, quietCount; + int moveCount, captureCount, quietCount, bestMoveCount, improvement, complexity; // Step 1. Initialize node - Thread* thisThread = pos.this_thread(); - inCheck = pos.checkers(); - priorCapture = pos.captured_piece(); - Color us = pos.side_to_move(); - moveCount = captureCount = quietCount = ss->moveCount = 0; - bestValue = -VALUE_INFINITE; - maxValue = VALUE_INFINITE; + ss->inCheck = pos.checkers(); + priorCapture = pos.captured_piece(); + Color us = pos.side_to_move(); + moveCount = bestMoveCount = captureCount = quietCount = ss->moveCount = 0; + bestValue = -VALUE_INFINITE; + maxValue = VALUE_INFINITE; // Check for the available remaining time if (thisThread == Threads.main()) @@ -653,8 +617,8 @@ namespace { if ( Threads.stop.load(std::memory_order_relaxed) || pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !inCheck) ? evaluate(pos) - : value_draw(pos.this_thread()); + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) + : value_draw(pos.this_thread()); // Step 3. Mate distance pruning. Even if we mate at the next move our score // would be at best mate_in(ss->ply+1), but if alpha is already bigger because @@ -667,60 +631,65 @@ namespace { if (alpha >= beta) return alpha; } + else + thisThread->rootDelta = beta - alpha; assert(0 <= ss->ply && ss->ply < MAX_PLY); - (ss+1)->ply = ss->ply + 1; + (ss+1)->ttPv = false; (ss+1)->excludedMove = bestMove = MOVE_NONE; - (ss+2)->killers[0] = (ss+2)->killers[1] = MOVE_NONE; - Square prevSq = to_sq((ss-1)->currentMove); + (ss+2)->killers[0] = (ss+2)->killers[1] = MOVE_NONE; + ss->doubleExtensions = (ss-1)->doubleExtensions; + ss->depth = depth; + Square prevSq = to_sq((ss-1)->currentMove); + + // Update the running average statistics for double extensions + thisThread->doubleExtensionAverage[us].update(ss->depth > (ss-1)->depth); // Initialize statScore to zero for the grandchildren of the current position. // So statScore is shared between all grandchildren and only the first grandchild // starts with statScore = 0. Later grandchildren start with the last calculated // statScore of the previous grandchild. This influences the reduction rules in // LMR which are based on the statScore of parent position. - if (rootNode) - (ss+4)->statScore = 0; - else + if (!rootNode) (ss+2)->statScore = 0; // Step 4. Transposition table lookup. We don't want the score of a partial // search to overwrite a previous full search TT value, so we use a different // position key in case of an excluded move. excludedMove = ss->excludedMove; - posKey = pos.key() ^ Key(excludedMove << 16); // Isn't a very good hash - tte = TT.probe(posKey, ttHit); - ttValue = ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; + posKey = excludedMove == MOVE_NONE ? pos.key() : pos.key() ^ make_key(excludedMove); + tte = TT.probe(posKey, ss->ttHit); + ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; ttMove = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] - : ttHit ? tte->move() : MOVE_NONE; - ttPv = PvNode || (ttHit && tte->is_pv()); - // thisThread->ttHitAverage can be used to approximate the running average of ttHit - thisThread->ttHitAverage = (ttHitAverageWindow - 1) * thisThread->ttHitAverage / ttHitAverageWindow - + ttHitAverageResolution * ttHit; + : ss->ttHit ? tte->move() : MOVE_NONE; + ttCapture = ttMove && pos.capture_or_promotion(ttMove); + if (!excludedMove) + ss->ttPv = PvNode || (ss->ttHit && tte->is_pv()); // At non-PV nodes we check for an early TT cutoff if ( !PvNode - && ttHit - && tte->depth() >= depth + && ss->ttHit + && tte->depth() > depth - (thisThread->id() % 2 == 1) && ttValue != VALUE_NONE // Possible in case of TT access race && (ttValue >= beta ? (tte->bound() & BOUND_LOWER) : (tte->bound() & BOUND_UPPER))) { - // If ttMove is quiet, update move sorting heuristics on TT hit + // If ttMove is quiet, update move sorting heuristics on TT hit (~1 Elo) if (ttMove) { if (ttValue >= beta) { - if (!pos.capture_or_promotion(ttMove)) + // Bonus for a quiet ttMove that fails high (~3 Elo) + if (!ttCapture) update_quiet_stats(pos, ss, ttMove, stat_bonus(depth)); - // Extra penalty for early quiet moves of the previous ply + // Extra penalty for early quiet moves of the previous ply (~0 Elo) if ((ss-1)->moveCount <= 2 && !priorCapture) update_continuation_histories(ss-1, pos.piece_on(prevSq), prevSq, -stat_bonus(depth + 1)); } - // Penalty for a quiet ttMove that fails low - else if (!pos.capture_or_promotion(ttMove)) + // Penalty for a quiet ttMove that fails low (~1 Elo) + else if (!ttCapture) { int penalty = -stat_bonus(depth); thisThread->mainHistory[us][from_to(ttMove)] << penalty; @@ -728,6 +697,8 @@ namespace { } } + // Partial workaround for the graph history interaction problem + // For high rule50 counts don't produce transposition table cutoffs. if (pos.rule50_count() < 90) return ttValue; } @@ -755,9 +726,10 @@ namespace { int drawScore = TB::UseRule50 ? 1 : 0; - value = wdl < -drawScore ? -VALUE_MATE + MAX_PLY + ss->ply + 1 - : wdl > drawScore ? VALUE_MATE - MAX_PLY - ss->ply - 1 - : VALUE_DRAW + 2 * wdl * drawScore; + // use the range VALUE_MATE_IN_MAX_PLY to VALUE_TB_WIN_IN_MAX_PLY to score + value = wdl < -drawScore ? VALUE_MATED_IN_MAX_PLY + ss->ply + 1 + : wdl > drawScore ? VALUE_MATE_IN_MAX_PLY - ss->ply - 1 + : VALUE_DRAW + 2 * wdl * drawScore; Bound b = wdl < -drawScore ? BOUND_UPPER : wdl > drawScore ? BOUND_LOWER : BOUND_EXACT; @@ -765,7 +737,7 @@ namespace { if ( b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha)) { - tte->save(posKey, value_to_tt(value, ss->ply), ttPv, b, + tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, b, std::min(MAX_PLY - 1, depth + 6), MOVE_NONE, VALUE_NONE); @@ -783,65 +755,78 @@ namespace { } } + CapturePieceToHistory& captureHistory = thisThread->captureHistory; + // Step 6. Static evaluation of the position - if (inCheck) + if (ss->inCheck) { + // Skip early pruning when in check ss->staticEval = eval = VALUE_NONE; improving = false; - goto moves_loop; // Skip early pruning when in check + improvement = 0; + complexity = 0; + goto moves_loop; } - else if (ttHit) + else if (ss->ttHit) { // Never assume anything about values stored in TT ss->staticEval = eval = tte->eval(); if (eval == VALUE_NONE) ss->staticEval = eval = evaluate(pos); + // Randomize draw evaluation if (eval == VALUE_DRAW) eval = value_draw(thisThread); - // Can ttValue be used as a better position evaluation? + // ttValue can be used as a better position evaluation (~4 Elo) if ( ttValue != VALUE_NONE && (tte->bound() & (ttValue > eval ? BOUND_LOWER : BOUND_UPPER))) eval = ttValue; } else { - if ((ss-1)->currentMove != MOVE_NULL) - { - int bonus = -(ss-1)->statScore / 512; + ss->staticEval = eval = evaluate(pos); - ss->staticEval = eval = evaluate(pos) + bonus; - } - else - ss->staticEval = eval = -(ss-1)->staticEval + 2 * Eval::Tempo; + // Save static evaluation into transposition table + if (!excludedMove) + tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE, eval); + } - tte->save(posKey, VALUE_NONE, ttPv, BOUND_NONE, DEPTH_NONE, MOVE_NONE, eval); + // Use static evaluation difference to improve quiet move ordering (~3 Elo) + if (is_ok((ss-1)->currentMove) && !(ss-1)->inCheck && !priorCapture) + { + int bonus = std::clamp(-16 * int((ss-1)->staticEval + ss->staticEval), -2000, 2000); + thisThread->mainHistory[~us][from_to((ss-1)->currentMove)] << bonus; } - // Step 7. Razoring (~1 Elo) - if ( !rootNode // The required rootNode PV handling is not available in qsearch - && depth < 2 - && eval <= alpha - RazorMargin) - return qsearch(pos, ss, alpha, beta); + // Set up the improvement variable, which is the difference between the current + // static evaluation and the previous static evaluation at our turn (if we were + // in check at our previous move we look at the move prior to it). The improvement + // margin and the improving flag are used in various pruning heuristics. + improvement = (ss-2)->staticEval != VALUE_NONE ? ss->staticEval - (ss-2)->staticEval + : (ss-4)->staticEval != VALUE_NONE ? ss->staticEval - (ss-4)->staticEval + : 200; - improving = (ss-2)->staticEval == VALUE_NONE ? (ss->staticEval >= (ss-4)->staticEval - || (ss-4)->staticEval == VALUE_NONE) : ss->staticEval >= (ss-2)->staticEval; + improving = improvement > 0; + complexity = abs(ss->staticEval - (us == WHITE ? eg_value(pos.psq_score()) : -eg_value(pos.psq_score()))); - // Step 8. Futility pruning: child node (~50 Elo) - if ( !PvNode - && depth < 6 + thisThread->complexityAverage.update(complexity); + + // Step 7. Futility pruning: child node (~25 Elo). + // The depth condition is important for mate finding. + if ( !ss->ttPv + && depth < 9 && eval - futility_margin(depth, improving) >= beta - && eval < VALUE_KNOWN_WIN) // Do not return unproven wins + && eval < 15000) // 50% larger than VALUE_KNOWN_WIN, but smaller than TB wins. return eval; - // Step 9. Null move search with verification search (~40 Elo) + // Step 8. Null move search with verification search (~22 Elo) if ( !PvNode && (ss-1)->currentMove != MOVE_NULL - && (ss-1)->statScore < 23397 + && (ss-1)->statScore < 23767 && eval >= beta && eval >= ss->staticEval - && ss->staticEval >= beta - 32 * depth + 292 - improving * 30 + && ss->staticEval >= beta - 20 * depth - improvement / 15 + 204 + complexity / 25 && !excludedMove && pos.non_pawn_material(us) && (ss->ply >= thisThread->nmpMinPly || us != thisThread->nmpColor)) @@ -849,7 +834,7 @@ namespace { assert(eval - beta >= 0); // Null move dynamic reduction based on depth and value - Depth R = (854 + 68 * depth) / 258 + std::min(int(eval - beta) / 192, 3); + Depth R = std::min(int(eval - beta) / 205, 3) + depth / 3 + 4; ss->currentMove = MOVE_NULL; ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; @@ -862,11 +847,11 @@ namespace { if (nullValue >= beta) { - // Do not return unproven mate scores - if (nullValue >= VALUE_MATE_IN_MAX_PLY) + // Do not return unproven mate or TB scores + if (nullValue >= VALUE_TB_WIN_IN_MAX_PLY) nullValue = beta; - if (thisThread->nmpMinPly || (abs(beta) < VALUE_KNOWN_WIN && depth < 13)) + if (thisThread->nmpMinPly || (abs(beta) < VALUE_KNOWN_WIN && depth < 14)) return nullValue; assert(!thisThread->nmpMinPly); // Recursive verification is not allowed @@ -885,29 +870,39 @@ namespace { } } - // Step 10. ProbCut (~10 Elo) + probCutBeta = beta + 209 - 44 * improving; + + // Step 9. ProbCut (~4 Elo) // If we have a good enough capture and a reduced search returns a value // much above beta, we can (almost) safely prune the previous move. if ( !PvNode - && depth >= 5 - && abs(beta) < VALUE_MATE_IN_MAX_PLY) + && depth > 4 + && abs(beta) < VALUE_TB_WIN_IN_MAX_PLY + // if value from transposition table is lower than probCutBeta, don't attempt probCut + // there and in further interactions with transposition table cutoff depth is set to depth - 3 + // because probCut search has depth set to depth - 4 but we also do a move before it + // so effective depth is equal to depth - 3 + && !( ss->ttHit + && tte->depth() >= depth - 3 + && ttValue != VALUE_NONE + && ttValue < probCutBeta)) { - Value raisedBeta = std::min(beta + 189 - 45 * improving, VALUE_INFINITE); - MovePicker mp(pos, ttMove, raisedBeta - ss->staticEval, &thisThread->captureHistory); - int probCutCount = 0; + assert(probCutBeta < VALUE_INFINITE); + + MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &captureHistory); + bool ttPv = ss->ttPv; + ss->ttPv = false; - while ( (move = mp.next_move()) != MOVE_NONE - && probCutCount < 2 + 2 * cutNode) + while ((move = mp.next_move()) != MOVE_NONE) if (move != excludedMove && pos.legal(move)) { assert(pos.capture_or_promotion(move)); assert(depth >= 5); captureOrPromotion = true; - probCutCount++; ss->currentMove = move; - ss->continuationHistory = &thisThread->continuationHistory[inCheck] + ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck] [captureOrPromotion] [pos.moved_piece(move)] [to_sq(move)]; @@ -915,30 +910,56 @@ namespace { pos.do_move(move, st); // Perform a preliminary qsearch to verify that the move holds - value = -qsearch(pos, ss+1, -raisedBeta, -raisedBeta+1); + value = -qsearch(pos, ss+1, -probCutBeta, -probCutBeta+1); // If the qsearch held, perform the regular search - if (value >= raisedBeta) - value = -search(pos, ss+1, -raisedBeta, -raisedBeta+1, depth - 4, !cutNode); + if (value >= probCutBeta) + value = -search(pos, ss+1, -probCutBeta, -probCutBeta+1, depth - 4, !cutNode); pos.undo_move(move); - if (value >= raisedBeta) + if (value >= probCutBeta) + { + // if transposition table doesn't have equal or more deep info write probCut data into it + if ( !(ss->ttHit + && tte->depth() >= depth - 3 + && ttValue != VALUE_NONE)) + tte->save(posKey, value_to_tt(value, ss->ply), ttPv, + BOUND_LOWER, + depth - 3, move, ss->staticEval); return value; + } } + ss->ttPv = ttPv; } - // Step 11. Internal iterative deepening (~1 Elo) - if (depth >= 7 && !ttMove) - { - search(pos, ss, alpha, beta, depth - 7, cutNode); - - tte = TT.probe(posKey, ttHit); - ttValue = ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = ttHit ? tte->move() : MOVE_NONE; - } + // Step 10. If the position is not in TT, decrease depth by 2 or 1 depending on node type (~3 Elo) + if ( PvNode + && depth >= 6 + && !ttMove) + depth -= 2; + + if ( cutNode + && depth >= 9 + && !ttMove) + depth--; + +moves_loop: // When in check, search starts here + + // Step 11. A small Probcut idea, when we are in check (~0 Elo) + probCutBeta = beta + 409; + if ( ss->inCheck + && !PvNode + && depth >= 4 + && ttCapture + && (tte->bound() & BOUND_LOWER) + && tte->depth() >= depth - 3 + && ttValue >= probCutBeta + && abs(ttValue) <= VALUE_KNOWN_WIN + && abs(beta) <= VALUE_KNOWN_WIN + ) + return probCutBeta; -moves_loop: // When in check, search starts from here const PieceToHistory* contHist[] = { (ss-1)->continuationHistory, (ss-2)->continuationHistory, nullptr , (ss-4)->continuationHistory, @@ -947,17 +968,20 @@ namespace { Move countermove = thisThread->counterMoves[pos.piece_on(prevSq)][prevSq]; MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, - &thisThread->captureHistory, + &captureHistory, contHist, countermove, ss->killers); value = bestValue; - singularLMR = moveCountPruning = false; - ttCapture = ttMove && pos.capture_or_promotion(ttMove); + moveCountPruning = false; - // Mark this node as being searched - ThreadHolding th(thisThread, posKey, ss->ply); + // Indicate PvNodes that will probably fail low if the node was searched + // at a depth equal or greater than the current depth, and the result of this search was a fail low. + bool likelyFailLow = PvNode + && ttMove + && (tte->bound() & BOUND_UPPER) + && tte->depth() >= depth; // Step 12. Loop through all pseudo-legal moves until no moves remain // or a beta cutoff occurs. @@ -976,6 +1000,10 @@ namespace { thisThread->rootMoves.begin() + thisThread->pvLast, move)) continue; + // Check for legality + if (!rootNode && !pos.legal(move)) + continue; + ss->moveCount = ++moveCount; if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) @@ -993,71 +1021,93 @@ namespace { // Calculate new depth for this move newDepth = depth - 1; - // Step 13. Pruning at shallow depth (~200 Elo) + Value delta = beta - alpha; + + // Step 13. Pruning at shallow depth (~98 Elo). Depth conditions are important for mate finding. if ( !rootNode && pos.non_pawn_material(us) - && bestValue > VALUE_MATED_IN_MAX_PLY) + && bestValue > VALUE_TB_LOSS_IN_MAX_PLY) { - // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold + // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold (~7 Elo) moveCountPruning = moveCount >= futility_move_count(improving, depth); - if ( !captureOrPromotion - && !givesCheck) + // Reduced depth of the next LMR search + int lmrDepth = std::max(newDepth - reduction(improving, depth, moveCount, delta, thisThread->rootDelta), 0); + + if ( captureOrPromotion + || givesCheck) { - // Reduced depth of the next LMR search - int lmrDepth = std::max(newDepth - reduction(improving, depth, moveCount), 0); + // Futility pruning for captures (~0 Elo) + if ( !pos.empty(to_sq(move)) + && !givesCheck + && !PvNode + && lmrDepth < 6 + && !ss->inCheck + && ss->staticEval + 342 + 238 * lmrDepth + PieceValue[EG][pos.piece_on(to_sq(move))] + + captureHistory[movedPiece][to_sq(move)][type_of(pos.piece_on(to_sq(move)))] / 8 < alpha) + continue; - // Countermoves based pruning (~20 Elo) - if ( lmrDepth < 4 + ((ss-1)->statScore > 0 || (ss-1)->moveCount == 1) - && (*contHist[0])[movedPiece][to_sq(move)] < CounterMovePruneThreshold - && (*contHist[1])[movedPiece][to_sq(move)] < CounterMovePruneThreshold) + // SEE based pruning (~9 Elo) + if (!pos.see_ge(move, Value(-217) * depth)) continue; + } + else + { + int history = (*contHist[0])[movedPiece][to_sq(move)] + + (*contHist[1])[movedPiece][to_sq(move)] + + (*contHist[3])[movedPiece][to_sq(move)]; - // Futility pruning: parent node (~5 Elo) - if ( lmrDepth < 6 - && !inCheck - && ss->staticEval + 235 + 172 * lmrDepth <= alpha - && thisThread->mainHistory[us][from_to(move)] - + (*contHist[0])[movedPiece][to_sq(move)] - + (*contHist[1])[movedPiece][to_sq(move)] - + (*contHist[3])[movedPiece][to_sq(move)] < 25000) + // Continuation history based pruning (~2 Elo) + if ( lmrDepth < 5 + && history < -3875 * (depth - 1)) continue; - // Prune moves with negative SEE (~20 Elo) - if (!pos.see_ge(move, Value(-(32 - std::min(lmrDepth, 18)) * lmrDepth * lmrDepth))) + history += thisThread->mainHistory[us][from_to(move)]; + + // Futility pruning: parent node (~9 Elo) + if ( !ss->inCheck + && lmrDepth < 8 + && ss->staticEval + 138 + 137 * lmrDepth + history / 64 <= alpha) continue; - } - else if (!pos.see_ge(move, Value(-194) * depth)) // (~25 Elo) + + // Prune moves with negative SEE (~3 Elo) + if (!pos.see_ge(move, Value(-21 * lmrDepth * lmrDepth - 21 * lmrDepth))) continue; + } } - // Step 14. Extensions (~75 Elo) + // Step 14. Extensions (~66 Elo) - // Singular extension search (~70 Elo). If all moves but one fail low on a + // Singular extension search (~58 Elo). If all moves but one fail low on a // search of (alpha-s, beta-s), and just one fails high on (alpha, beta), // then that move is singular and should be extended. To verify this we do // a reduced search on all the other moves but the ttMove and if the - // result is lower than ttValue minus a margin then we will extend the ttMove. - if ( depth >= 6 + // result is lower than ttValue minus a margin, then we will extend the ttMove. + if ( !rootNode + && depth >= 6 + 2 * (PvNode && tte->is_pv()) && move == ttMove - && !rootNode && !excludedMove // Avoid recursive singular search /* && ttValue != VALUE_NONE Already implicit in the next condition */ && abs(ttValue) < VALUE_KNOWN_WIN && (tte->bound() & BOUND_LOWER) - && tte->depth() >= depth - 3 - && pos.legal(move)) + && tte->depth() >= depth - 3) { - Value singularBeta = ttValue - 2 * depth; - Depth halfDepth = depth / 2; + Value singularBeta = ttValue - 3 * depth; + Depth singularDepth = (depth - 1) / 2; + ss->excludedMove = move; - value = search(pos, ss, singularBeta - 1, singularBeta, halfDepth, cutNode); + value = search(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode); ss->excludedMove = MOVE_NONE; if (value < singularBeta) { extension = 1; - singularLMR = true; + + // Avoid search explosion by limiting the number of double extensions + if ( !PvNode + && value < singularBeta - 75 + && ss->doubleExtensions <= 6) + extension = 2; } // Multi-cut pruning @@ -1067,44 +1117,35 @@ namespace { // a soft bound. else if (singularBeta >= beta) return singularBeta; - } - // Check extension (~2 Elo) - else if ( givesCheck - && (pos.is_discovery_check_on_king(~us, move) || pos.see_ge(move))) - extension = 1; - - // Passed pawn extension - else if ( move == ss->killers[0] - && pos.advanced_pawn_push(move) - && pos.pawn_passed(us, to_sq(move))) - extension = 1; + // If the eval of ttMove is greater than beta, we reduce it (negative extension) + else if (ttValue >= beta) + extension = -2; + } - // Last captures extension - else if ( PieceValue[EG][pos.captured_piece()] > PawnValueEg - && pos.non_pawn_material() <= 2 * RookValueMg) + // Check extensions (~1 Elo) + else if ( givesCheck + && depth > 6 + && abs(ss->staticEval) > 100) extension = 1; - // Castling extension - if (type_of(move) == CASTLING) + // Quiet ttMove extensions (~0 Elo) + else if ( PvNode + && move == ttMove + && move == ss->killers[0] + && (*contHist[0])[movedPiece][to_sq(move)] >= 10000) extension = 1; // Add extension to new depth newDepth += extension; + ss->doubleExtensions = (ss-1)->doubleExtensions + (extension == 2); // Speculative prefetch as early as possible prefetch(TT.first_entry(pos.key_after(move))); - // Check for legality just before making the move - if (!rootNode && !pos.legal(move)) - { - ss->moveCount = --moveCount; - continue; - } - // Update the current move (this must be done after singular extension search) ss->currentMove = move; - ss->continuationHistory = &thisThread->continuationHistory[inCheck] + ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck] [captureOrPromotion] [movedPiece] [to_sq(move)]; @@ -1112,106 +1153,87 @@ namespace { // Step 15. Make the move pos.do_move(move, st, givesCheck); - // Step 16. Reduced depth search (LMR, ~200 Elo). If the move fails high it will be - // re-searched at full depth. + bool doDeeperSearch = false; + + // Step 16. Late moves reduction / extension (LMR, ~98 Elo) + // We use various heuristics for the sons of a node after the first son has + // been searched. In general we would like to reduce them, but there are many + // cases where we extend a son if it has good chances to be "interesting". if ( depth >= 3 - && moveCount > 1 + rootNode + (rootNode && bestValue < alpha) - && (!rootNode || thisThread->best_move_count(move) == 0) - && ( !captureOrPromotion - || moveCountPruning - || ss->staticEval + PieceValue[EG][pos.captured_piece()] <= alpha - || cutNode - || thisThread->ttHitAverage < 375 * ttHitAverageResolution * ttHitAverageWindow / 1024)) + && moveCount > 1 + 2 * rootNode + && ( !ss->ttPv + || !captureOrPromotion + || (cutNode && (ss-1)->moveCount > 1))) { - Depth r = reduction(improving, depth, moveCount); + Depth r = reduction(improving, depth, moveCount, delta, thisThread->rootDelta); - // Decrease reduction if the ttHit running average is large - if (thisThread->ttHitAverage > 500 * ttHitAverageResolution * ttHitAverageWindow / 1024) + // Decrease reduction at some PvNodes (~2 Elo) + if ( PvNode + && bestMoveCount <= 3) r--; - // Reduction if other threads are searching this position. - if (th.marked()) - r++; - - // Decrease reduction if position is or has been on the PV (~10 Elo) - if (ttPv) + // Decrease reduction if position is or has been on the PV + // and node is not likely to fail low. (~3 Elo) + if ( ss->ttPv + && !likelyFailLow) r -= 2; - // Decrease reduction if opponent's move count is high (~5 Elo) - if ((ss-1)->moveCount > 14) + // Decrease reduction if opponent's move count is high (~1 Elo) + if ((ss-1)->moveCount > 13) r--; - // Decrease reduction if ttMove has been singularly extended (~3 Elo) - if (singularLMR) - r -= 2; - - if (!captureOrPromotion) - { - // Increase reduction if ttMove is a capture (~5 Elo) - if (ttCapture) - r++; - - // Increase reduction for cut nodes (~10 Elo) - if (cutNode) - r += 2; - - // Decrease reduction for moves that escape a capture. Filter out - // castling moves, because they are coded as "king captures rook" and - // hence break make_move(). (~2 Elo) - else if ( type_of(move) == NORMAL - && !pos.see_ge(reverse_move(move))) - r -= 2; - - ss->statScore = thisThread->mainHistory[us][from_to(move)] - + (*contHist[0])[movedPiece][to_sq(move)] - + (*contHist[1])[movedPiece][to_sq(move)] - + (*contHist[3])[movedPiece][to_sq(move)] - - 4926; - - // Reset statScore to zero if negative and most stats shows >= 0 - if ( ss->statScore < 0 - && (*contHist[0])[movedPiece][to_sq(move)] >= 0 - && (*contHist[1])[movedPiece][to_sq(move)] >= 0 - && thisThread->mainHistory[us][from_to(move)] >= 0) - ss->statScore = 0; - - // Decrease/increase reduction by comparing opponent's stat score (~10 Elo) - if (ss->statScore >= -102 && (ss-1)->statScore < -114) - r--; - - else if ((ss-1)->statScore >= -116 && ss->statScore < -154) - r++; - - // Decrease/increase reduction for moves with a good/bad history (~30 Elo) - r -= ss->statScore / 16384; - } + // Increase reduction for cut nodes (~3 Elo) + if (cutNode && move != ss->killers[0]) + r += 2; - // Increase reduction for captures/promotions if late move and at low depth - else if (depth < 8 && moveCount > 2) + // Increase reduction if ttMove is a capture (~3 Elo) + if (ttCapture) r++; - Depth d = clamp(newDepth - r, 1, newDepth); + ss->statScore = thisThread->mainHistory[us][from_to(move)] + + (*contHist[0])[movedPiece][to_sq(move)] + + (*contHist[1])[movedPiece][to_sq(move)] + + (*contHist[3])[movedPiece][to_sq(move)] + - 4923; + + // Decrease/increase reduction for moves with a good/bad history (~30 Elo) + r -= ss->statScore / 14721; + + // In general we want to cap the LMR depth search at newDepth. But if reductions + // are really negative and movecount is low, we allow this move to be searched + // deeper than the first move (this may lead to hidden double extensions). + int deeper = r >= -1 ? 0 + : moveCount <= 5 ? 2 + : PvNode && depth > 6 ? 1 + : cutNode && moveCount <= 7 ? 1 + : 0; + + Depth d = std::clamp(newDepth - r, 1, newDepth + deeper); value = -search(pos, ss+1, -(alpha+1), -alpha, d, true); - doFullDepthSearch = (value > alpha && d != newDepth), didLMR = true; + // If the son is reduced and fails high it will be re-searched at full depth + doFullDepthSearch = value > alpha && d < newDepth; + doDeeperSearch = value > (alpha + 62 + 20 * (newDepth - d)); + didLMR = true; } else - doFullDepthSearch = !PvNode || moveCount > 1, didLMR = false; + { + doFullDepthSearch = !PvNode || moveCount > 1; + didLMR = false; + } // Step 17. Full depth search when LMR is skipped or fails high if (doFullDepthSearch) { - value = -search(pos, ss+1, -(alpha+1), -alpha, newDepth, !cutNode); + value = -search(pos, ss+1, -(alpha+1), -alpha, newDepth + doDeeperSearch, !cutNode); + // If the move passed LMR update its stats if (didLMR && !captureOrPromotion) { int bonus = value > alpha ? stat_bonus(newDepth) : -stat_bonus(newDepth); - if (move == ss->killers[0]) - bonus += bonus / 4; - update_continuation_histories(ss, movedPiece, to_sq(move), bonus); } } @@ -1224,7 +1246,8 @@ namespace { (ss+1)->pv = pv; (ss+1)->pv[0] = MOVE_NONE; - value = -search(pos, ss+1, -beta, -alpha, newDepth, false); + value = -search(pos, ss+1, -beta, -alpha, + std::min(maxNextDepth, newDepth), false); } // Step 18. Undo move @@ -1244,6 +1267,8 @@ namespace { RootMove& rm = *std::find(thisThread->rootMoves.begin(), thisThread->rootMoves.end(), move); + rm.averageScore = rm.averageScore != -VALUE_INFINITE ? (2 * value + rm.averageScore) / 3 : value; + // PV move or new best move? if (moveCount == 1 || value > alpha) { @@ -1256,10 +1281,11 @@ namespace { for (Move* m = (ss+1)->pv; *m != MOVE_NONE; ++m) rm.pv.push_back(*m); - // We record how often the best move has been changed in each - // iteration. This information is used for time management: When - // the best move changes frequently, we allocate some more time. - if (moveCount > 1) + // We record how often the best move has been changed in each iteration. + // This information is used for time management. In MultiPV mode, + // we must take care to only do this for the first PV line. + if ( moveCount > 1 + && !thisThread->pvIdx) ++thisThread->bestMoveChanges; } else @@ -1281,16 +1307,19 @@ namespace { update_pv(ss->pv, move, (ss+1)->pv); if (PvNode && value < beta) // Update alpha! Always alpha < beta + { alpha = value; + bestMoveCount++; + } else { assert(value >= beta); // Fail high - ss->statScore = 0; break; } } } + // If the move is worse than some previously searched move, remember it to update its stats later if (move != bestMove) { if (captureOrPromotion && captureCount < 32) @@ -1314,12 +1343,14 @@ namespace { // must be a mate or a stalemate. If we are in a singular extension search then // return a fail low score. - assert(moveCount || !inCheck || excludedMove || !MoveList(pos).size()); + assert(moveCount || !ss->inCheck || excludedMove || !MoveList(pos).size()); if (!moveCount) - bestValue = excludedMove ? alpha - : inCheck ? mated_in(ss->ply) : VALUE_DRAW; + bestValue = excludedMove ? alpha : + ss->inCheck ? mated_in(ss->ply) + : VALUE_DRAW; + // If there is a move which produces search value greater than alpha we update stats of searched moves else if (bestMove) update_all_stats(pos, ss, bestMove, bestValue, beta, prevSq, quietsSearched, quietCount, capturesSearched, captureCount, depth); @@ -1327,13 +1358,31 @@ namespace { // Bonus for prior countermove that caused the fail low else if ( (depth >= 3 || PvNode) && !priorCapture) - update_continuation_histories(ss-1, pos.piece_on(prevSq), prevSq, stat_bonus(depth)); + { + //Assign extra bonus if current node is PvNode or cutNode + //or fail low was really bad + bool extraBonus = PvNode + || cutNode + || bestValue < alpha - 94 * depth; + + update_continuation_histories(ss-1, pos.piece_on(prevSq), prevSq, stat_bonus(depth) * (1 + extraBonus)); + } if (PvNode) bestValue = std::min(bestValue, maxValue); - if (!excludedMove) - tte->save(posKey, value_to_tt(bestValue, ss->ply), ttPv, + // If no good move is found and the previous position was ttPv, then the previous + // opponent move is probably good and the new position is added to the search tree. + if (bestValue <= alpha) + ss->ttPv = ss->ttPv || ((ss-1)->ttPv && depth > 3); + // Otherwise, a counter move has been found and if the position is the last leaf + // in the search tree, remove the position from the search tree. + else if (depth > 3) + ss->ttPv = ss->ttPv && (ss+1)->ttPv; + + // Write gathered information in transposition table + if (!excludedMove && !(rootNode && thisThread->pvIdx)) + tte->save(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv, bestValue >= beta ? BOUND_LOWER : PvNode && bestMove ? BOUND_EXACT : BOUND_UPPER, depth, bestMove, ss->staticEval); @@ -1346,10 +1395,11 @@ namespace { // qsearch() is the quiescence search function, which is called by the main search // function with zero depth, or recursively with further decreasing depth per call. - template + template Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { - constexpr bool PvNode = NT == PV; + static_assert(nodeType != Root); + constexpr bool PvNode = nodeType == PV; assert(alpha >= -VALUE_INFINITE && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); @@ -1357,48 +1407,48 @@ namespace { Move pv[MAX_PLY+1]; StateInfo st; + ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); + TTEntry* tte; Key posKey; Move ttMove, move, bestMove; Depth ttDepth; - Value bestValue, value, ttValue, futilityValue, futilityBase, oldAlpha; - bool ttHit, pvHit, inCheck, givesCheck, captureOrPromotion, evasionPrunable; + Value bestValue, value, ttValue, futilityValue, futilityBase; + bool pvHit, givesCheck, captureOrPromotion; int moveCount; if (PvNode) { - oldAlpha = alpha; // To flag BOUND_EXACT when eval above alpha and no available moves (ss+1)->pv = pv; ss->pv[0] = MOVE_NONE; } Thread* thisThread = pos.this_thread(); - (ss+1)->ply = ss->ply + 1; bestMove = MOVE_NONE; - inCheck = pos.checkers(); + ss->inCheck = pos.checkers(); moveCount = 0; // Check for an immediate draw or maximum ply reached if ( pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !inCheck) ? evaluate(pos) : VALUE_DRAW; + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) : VALUE_DRAW; assert(0 <= ss->ply && ss->ply < MAX_PLY); // Decide whether or not to include checks: this fixes also the type of // TT entry depth that we are going to use. Note that in qsearch we use // only two types of depth in TT: DEPTH_QS_CHECKS or DEPTH_QS_NO_CHECKS. - ttDepth = inCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS + ttDepth = ss->inCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS : DEPTH_QS_NO_CHECKS; // Transposition table lookup posKey = pos.key(); - tte = TT.probe(posKey, ttHit); - ttValue = ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; - ttMove = ttHit ? tte->move() : MOVE_NONE; - pvHit = ttHit && tte->is_pv(); + tte = TT.probe(posKey, ss->ttHit); + ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; + ttMove = ss->ttHit ? tte->move() : MOVE_NONE; + pvHit = ss->ttHit && tte->is_pv(); if ( !PvNode - && ttHit + && ss->ttHit && tte->depth() >= ttDepth && ttValue != VALUE_NONE // Only in case of TT access race && (ttValue >= beta ? (tte->bound() & BOUND_LOWER) @@ -1406,34 +1456,36 @@ namespace { return ttValue; // Evaluate the position statically - if (inCheck) + if (ss->inCheck) { ss->staticEval = VALUE_NONE; bestValue = futilityBase = -VALUE_INFINITE; } else { - if (ttHit) + if (ss->ttHit) { // Never assume anything about values stored in TT if ((ss->staticEval = bestValue = tte->eval()) == VALUE_NONE) ss->staticEval = bestValue = evaluate(pos); - // Can ttValue be used as a better position evaluation? + // ttValue can be used as a better position evaluation (~7 Elo) if ( ttValue != VALUE_NONE && (tte->bound() & (ttValue > bestValue ? BOUND_LOWER : BOUND_UPPER))) bestValue = ttValue; } else + // In case of null move search use previous static eval with a different sign ss->staticEval = bestValue = (ss-1)->currentMove != MOVE_NULL ? evaluate(pos) - : -(ss-1)->staticEval + 2 * Eval::Tempo; + : -(ss-1)->staticEval; // Stand pat. Return immediately if static value is at least beta if (bestValue >= beta) { - if (!ttHit) - tte->save(posKey, value_to_tt(bestValue, ss->ply), pvHit, BOUND_LOWER, + // Save gathered info in transposition table + if (!ss->ttHit) + tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE, MOVE_NONE, ss->staticEval); return bestValue; @@ -1442,7 +1494,7 @@ namespace { if (PvNode && bestValue > alpha) alpha = bestValue; - futilityBase = bestValue + 154; + futilityBase = bestValue + 155; } const PieceToHistory* contHist[] = { (ss-1)->continuationHistory, (ss-2)->continuationHistory, @@ -1451,30 +1503,38 @@ namespace { // Initialize a MovePicker object for the current position, and prepare // to search the moves. Because the depth is <= 0 here, only captures, - // queen promotions and checks (only if depth >= DEPTH_QS_CHECKS) will - // be generated. + // queen promotions, and other checks (only if depth >= DEPTH_QS_CHECKS) + // will be generated. + Square prevSq = to_sq((ss-1)->currentMove); MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, contHist, - to_sq((ss-1)->currentMove)); + prevSq); // Loop through the moves until no moves remain or a beta cutoff occurs while ((move = mp.next_move()) != MOVE_NONE) { assert(is_ok(move)); + // Check for legality + if (!pos.legal(move)) + continue; + givesCheck = pos.gives_check(move); captureOrPromotion = pos.capture_or_promotion(move); moveCount++; - // Futility pruning - if ( !inCheck + // Futility pruning and moveCount pruning (~5 Elo) + if ( bestValue > VALUE_TB_LOSS_IN_MAX_PLY && !givesCheck + && to_sq(move) != prevSq && futilityBase > -VALUE_KNOWN_WIN - && !pos.advanced_pawn_push(move)) + && type_of(move) != PROMOTION) { - assert(type_of(move) != ENPASSANT); // Due to !pos.advanced_pawn_push + + if (moveCount > 2) + continue; futilityValue = futilityBase + PieceValue[EG][pos.piece_on(to_sq(move))]; @@ -1491,35 +1551,30 @@ namespace { } } - // Detect non-capture evasions that are candidates to be pruned - evasionPrunable = inCheck - && (depth != 0 || moveCount > 2) - && bestValue > VALUE_MATED_IN_MAX_PLY - && !pos.capture(move); - - // Don't search moves with negative SEE values - if ( (!inCheck || evasionPrunable) && !pos.see_ge(move)) + // Do not search moves with negative SEE values (~5 Elo) + if ( bestValue > VALUE_TB_LOSS_IN_MAX_PLY + && !pos.see_ge(move)) continue; // Speculative prefetch as early as possible prefetch(TT.first_entry(pos.key_after(move))); - // Check for legality just before making the move - if (!pos.legal(move)) - { - moveCount--; - continue; - } - ss->currentMove = move; - ss->continuationHistory = &thisThread->continuationHistory[inCheck] + ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck] [captureOrPromotion] [pos.moved_piece(move)] [to_sq(move)]; + // Continuation history based pruning (~2 Elo) + if ( !captureOrPromotion + && bestValue > VALUE_TB_LOSS_IN_MAX_PLY + && (*contHist[0])[pos.moved_piece(move)][to_sq(move)] < CounterMovePruneThreshold + && (*contHist[1])[pos.moved_piece(move)][to_sq(move)] < CounterMovePruneThreshold) + continue; + // Make and search the move pos.do_move(move, st, givesCheck); - value = -qsearch(pos, ss+1, -beta, -alpha, depth - 1); + value = -qsearch(pos, ss+1, -beta, -alpha, depth - 1); pos.undo_move(move); assert(value > -VALUE_INFINITE && value < VALUE_INFINITE); @@ -1544,14 +1599,18 @@ namespace { } } - // All legal moves have been searched. A special case: If we're in check + // All legal moves have been searched. A special case: if we're in check // and no legal moves were found, it is checkmate. - if (inCheck && bestValue == -VALUE_INFINITE) + if (ss->inCheck && bestValue == -VALUE_INFINITE) + { + assert(!MoveList(pos).size()); + return mated_in(ss->ply); // Plies to mate from the root + } + // Save gathered info in transposition table tte->save(posKey, value_to_tt(bestValue, ss->ply), pvHit, - bestValue >= beta ? BOUND_LOWER : - PvNode && bestValue > oldAlpha ? BOUND_EXACT : BOUND_UPPER, + bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove, ss->staticEval); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1560,28 +1619,47 @@ namespace { } - // value_to_tt() adjusts a mate score from "plies to mate from the root" to - // "plies to mate from the current position". Non-mate scores are unchanged. + // value_to_tt() adjusts a mate or TB score from "plies to mate from the root" to + // "plies to mate from the current position". Standard scores are unchanged. // The function is called before storing a value in the transposition table. Value value_to_tt(Value v, int ply) { assert(v != VALUE_NONE); - return v >= VALUE_MATE_IN_MAX_PLY ? v + ply - : v <= VALUE_MATED_IN_MAX_PLY ? v - ply : v; + return v >= VALUE_TB_WIN_IN_MAX_PLY ? v + ply + : v <= VALUE_TB_LOSS_IN_MAX_PLY ? v - ply : v; } - // value_from_tt() is the inverse of value_to_tt(): It adjusts a mate score - // from the transposition table (which refers to the plies to mate/be mated - // from current position) to "plies to mate/be mated from the root". + // value_from_tt() is the inverse of value_to_tt(): it adjusts a mate or TB score + // from the transposition table (which refers to the plies to mate/be mated from + // current position) to "plies to mate/be mated (TB win/loss) from the root". However, + // for mate scores, to avoid potentially false mate scores related to the 50 moves rule + // and the graph history interaction, we return an optimal TB score instead. Value value_from_tt(Value v, int ply, int r50c) { - return v == VALUE_NONE ? VALUE_NONE - : v >= VALUE_MATE_IN_MAX_PLY ? VALUE_MATE - v > 99 - r50c ? VALUE_MATE_IN_MAX_PLY : v - ply - : v <= VALUE_MATED_IN_MAX_PLY ? VALUE_MATE + v > 99 - r50c ? VALUE_MATED_IN_MAX_PLY : v + ply : v; + if (v == VALUE_NONE) + return VALUE_NONE; + + if (v >= VALUE_TB_WIN_IN_MAX_PLY) // TB win or better + { + if (v >= VALUE_MATE_IN_MAX_PLY && VALUE_MATE - v > 99 - r50c) + return VALUE_MATE_IN_MAX_PLY - 1; // do not return a potentially false mate score + + return v - ply; + } + + if (v <= VALUE_TB_LOSS_IN_MAX_PLY) // TB loss or worse + { + if (v <= VALUE_MATED_IN_MAX_PLY && VALUE_MATE + v > 99 - r50c) + return VALUE_MATED_IN_MAX_PLY + 1; // do not return a potentially false mate score + + return v + ply; + } + + return v; } @@ -1613,9 +1691,10 @@ namespace { if (!pos.capture_or_promotion(bestMove)) { + // Increase stats for the best move in case it was a quiet move update_quiet_stats(pos, ss, bestMove, bonus2); - // Decrease all the non-best quiet moves + // Decrease stats for all non-best quiet moves for (int i = 0; i < quietCount; ++i) { thisThread->mainHistory[us][from_to(quietsSearched[i])] << -bonus2; @@ -1623,14 +1702,16 @@ namespace { } } else + // Increase stats for the best move in case it was a capture move captureHistory[moved_piece][to_sq(bestMove)][captured] << bonus1; - // Extra penalty for a quiet TT or main killer move in previous ply when it gets refuted - if ( ((ss-1)->moveCount == 1 || ((ss-1)->currentMove == (ss-1)->killers[0])) + // Extra penalty for a quiet early move that was not a TT move or + // main killer move in previous ply when it gets refuted. + if ( ((ss-1)->moveCount == 1 + (ss-1)->ttHit || ((ss-1)->currentMove == (ss-1)->killers[0])) && !pos.captured_piece()) update_continuation_histories(ss-1, pos.piece_on(prevSq), prevSq, -bonus1); - // Decrease all the non-best capture moves + // Decrease stats for all non-best capture moves for (int i = 0; i < captureCount; ++i) { moved_piece = pos.moved_piece(capturesSearched[i]); @@ -1641,13 +1722,18 @@ namespace { // update_continuation_histories() updates histories of the move pairs formed - // by moves at ply -1, -2, and -4 with current move. + // by moves at ply -1, -2, -4, and -6 with current move. void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { for (int i : {1, 2, 4, 6}) + { + // Only update first 2 continuation histories if we are in check + if (ss->inCheck && i > 2) + break; if (is_ok((ss-i)->currentMove)) (*(ss-i)->continuationHistory)[pc][to] << bonus; + } } @@ -1655,6 +1741,7 @@ namespace { void update_quiet_stats(const Position& pos, Stack* ss, Move move, int bonus) { + // Update killers if (ss->killers[0] != move) { ss->killers[1] = ss->killers[0]; @@ -1666,9 +1753,7 @@ namespace { thisThread->mainHistory[us][from_to(move)] << bonus; update_continuation_histories(ss, pos.moved_piece(move), to_sq(move), bonus); - if (type_of(pos.moved_piece(move)) != PAWN) - thisThread->mainHistory[us][from_to(reverse_move(move))] << -bonus; - + // Update countermove history if (is_ok((ss-1)->currentMove)) { Square prevSq = to_sq((ss-1)->currentMove); @@ -1687,8 +1772,8 @@ namespace { // RootMoves are already sorted by score in descending order Value topScore = rootMoves[0].score; int delta = std::min(topScore - rootMoves[multiPV - 1].score, PawnValueMg); - int weakness = 120 - 2 * level; int maxScore = -VALUE_INFINITE; + double weakness = 120 - 2 * level; // Choose best move. For each move score we add two terms, both dependent on // weakness. One is deterministic and bigger for weaker levels, and one is @@ -1696,8 +1781,8 @@ namespace { for (size_t i = 0; i < multiPV; ++i) { // This is our magic formula - int push = ( weakness * int(topScore - rootMoves[i].score) - + delta * (rng.rand() % weakness)) / 128; + int push = int(( weakness * int(topScore - rootMoves[i].score) + + delta * (rng.rand() % int(weakness))) / 128); if (rootMoves[i].score + push >= maxScore) { @@ -1711,6 +1796,7 @@ namespace { } // namespace + /// MainThread::check_time() is used to print debug info and, more importantly, /// to detect when we are out of available time and thus stop the search. @@ -1761,13 +1847,16 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) { { bool updated = rootMoves[i].score != -VALUE_INFINITE; - if (depth == 1 && !updated) + if (depth == 1 && !updated && i > 0) continue; - Depth d = updated ? depth : depth - 1; + Depth d = updated ? depth : std::max(1, depth - 1); Value v = updated ? rootMoves[i].score : rootMoves[i].previousScore; - bool tb = TB::RootInTB && abs(v) < VALUE_MATE - MAX_PLY; + if (v == -VALUE_INFINITE) + v = VALUE_ZERO; + + bool tb = TB::RootInTB && abs(v) < VALUE_MATE_IN_MAX_PLY; v = tb ? rootMoves[i].tbScore : v; if (ss.rdbuf()->in_avail()) // Not at first line @@ -1779,6 +1868,9 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) { << " multipv " << i + 1 << " score " << UCI::value(v); + if (Options["UCI_ShowWDL"]) + ss << UCI::wdl(v, pos.game_ply()); + if (!tb && i == pvIdx) ss << (v >= beta ? " lowerbound" : v <= alpha ? " upperbound" : ""); @@ -1808,6 +1900,8 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) { bool RootMove::extract_ponder_from_tt(Position& pos) { StateInfo st; + ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); + bool ttHit; assert(pv.size() == 1); @@ -1861,7 +1955,7 @@ void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) { if (RootInTB) { // Sort moves according to TB rank - std::sort(rootMoves.begin(), rootMoves.end(), + std::stable_sort(rootMoves.begin(), rootMoves.end(), [](const RootMove &a, const RootMove &b) { return a.tbRank > b.tbRank; } ); // Probe during search only if DTZ is not available and we are winning @@ -1875,3 +1969,5 @@ void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) { m.tbRank = 0; } } + +} // namespace Stockfish diff --git a/src/search.h b/src/search.h index a900d094b0c..806295a1100 100644 --- a/src/search.h +++ b/src/search.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,6 +25,8 @@ #include "movepick.h" #include "types.h" +namespace Stockfish { + class Position; namespace Search { @@ -47,8 +47,13 @@ struct Stack { Move excludedMove; Move killers[2]; Value staticEval; + Depth depth; int statScore; int moveCount; + bool inCheck; + bool ttPv; + bool ttHit; + int doubleExtensions; }; @@ -68,9 +73,9 @@ struct RootMove { Value score = -VALUE_INFINITE; Value previousScore = -VALUE_INFINITE; + Value averageScore = -VALUE_INFINITE; int selDepth = 0; int tbRank = 0; - int bestMoveCount = 0; Value tbScore; std::vector pv; }; @@ -90,7 +95,7 @@ struct LimitsType { } bool use_time_management() const { - return !(mate | movetime | depth | nodes | perft | infinite); + return time[WHITE] || time[BLACK]; } std::vector searchmoves; @@ -106,4 +111,6 @@ void clear(); } // namespace Search +} // namespace Stockfish + #endif // #ifndef SEARCH_H_INCLUDED diff --git a/src/simd.h b/src/simd.h new file mode 100644 index 00000000000..7b9e8fb23b7 --- /dev/null +++ b/src/simd.h @@ -0,0 +1,387 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef STOCKFISH_SIMD_H_INCLUDED +#define STOCKFISH_SIMD_H_INCLUDED + +#if defined(USE_AVX2) +# include + +#elif defined(USE_SSE41) +# include + +#elif defined(USE_SSSE3) +# include + +#elif defined(USE_SSE2) +# include + +#elif defined(USE_MMX) +# include + +#elif defined(USE_NEON) +# include +#endif + +// The inline asm is only safe for GCC, where it is necessary to get good codegen. +// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101693 +// Clang does fine without it. +// Play around here: https://godbolt.org/z/7EWqrYq51 +#if (defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER)) +#define USE_INLINE_ASM +#endif + +// Use either the AVX512 or AVX-VNNI version of the VNNI instructions. +#if defined(USE_AVXVNNI) +#define VNNI_PREFIX "%{vex%} " +#else +#define VNNI_PREFIX "" +#endif + +namespace Stockfish::Simd { + +#if defined (USE_AVX512) + + [[maybe_unused]] static int m512_hadd(__m512i sum, int bias) { + return _mm512_reduce_add_epi32(sum) + bias; + } + + /* + Parameters: + sum0 = [zmm0.i128[0], zmm0.i128[1], zmm0.i128[2], zmm0.i128[3]] + sum1 = [zmm1.i128[0], zmm1.i128[1], zmm1.i128[2], zmm1.i128[3]] + sum2 = [zmm2.i128[0], zmm2.i128[1], zmm2.i128[2], zmm2.i128[3]] + sum3 = [zmm3.i128[0], zmm3.i128[1], zmm3.i128[2], zmm3.i128[3]] + + Returns: + ret = [ + reduce_add_epi32(zmm0.i128[0]), reduce_add_epi32(zmm1.i128[0]), reduce_add_epi32(zmm2.i128[0]), reduce_add_epi32(zmm3.i128[0]), + reduce_add_epi32(zmm0.i128[1]), reduce_add_epi32(zmm1.i128[1]), reduce_add_epi32(zmm2.i128[1]), reduce_add_epi32(zmm3.i128[1]), + reduce_add_epi32(zmm0.i128[2]), reduce_add_epi32(zmm1.i128[2]), reduce_add_epi32(zmm2.i128[2]), reduce_add_epi32(zmm3.i128[2]), + reduce_add_epi32(zmm0.i128[3]), reduce_add_epi32(zmm1.i128[3]), reduce_add_epi32(zmm2.i128[3]), reduce_add_epi32(zmm3.i128[3]) + ] + */ + [[maybe_unused]] static __m512i m512_hadd128x16_interleave( + __m512i sum0, __m512i sum1, __m512i sum2, __m512i sum3) { + + __m512i sum01a = _mm512_unpacklo_epi32(sum0, sum1); + __m512i sum01b = _mm512_unpackhi_epi32(sum0, sum1); + + __m512i sum23a = _mm512_unpacklo_epi32(sum2, sum3); + __m512i sum23b = _mm512_unpackhi_epi32(sum2, sum3); + + __m512i sum01 = _mm512_add_epi32(sum01a, sum01b); + __m512i sum23 = _mm512_add_epi32(sum23a, sum23b); + + __m512i sum0123a = _mm512_unpacklo_epi64(sum01, sum23); + __m512i sum0123b = _mm512_unpackhi_epi64(sum01, sum23); + + return _mm512_add_epi32(sum0123a, sum0123b); + } + + [[maybe_unused]] static __m128i m512_haddx4( + __m512i sum0, __m512i sum1, __m512i sum2, __m512i sum3, + __m128i bias) { + + __m512i sum = m512_hadd128x16_interleave(sum0, sum1, sum2, sum3); + + __m256i sum256lo = _mm512_castsi512_si256(sum); + __m256i sum256hi = _mm512_extracti64x4_epi64(sum, 1); + + sum256lo = _mm256_add_epi32(sum256lo, sum256hi); + + __m128i sum128lo = _mm256_castsi256_si128(sum256lo); + __m128i sum128hi = _mm256_extracti128_si256(sum256lo, 1); + + return _mm_add_epi32(_mm_add_epi32(sum128lo, sum128hi), bias); + } + + [[maybe_unused]] static void m512_add_dpbusd_epi32( + __m512i& acc, + __m512i a, + __m512i b) { + +# if defined (USE_VNNI) +# if defined (USE_INLINE_ASM) + asm( + "vpdpbusd %[b], %[a], %[acc]\n\t" + : [acc]"+v"(acc) + : [a]"v"(a), [b]"vm"(b) + ); +# else + acc = _mm512_dpbusd_epi32(acc, a, b); +# endif +# else +# if defined (USE_INLINE_ASM) + __m512i tmp = _mm512_maddubs_epi16(a, b); + asm( + "vpmaddwd %[tmp], %[ones], %[tmp]\n\t" + "vpaddd %[acc], %[tmp], %[acc]\n\t" + : [acc]"+v"(acc), [tmp]"+&v"(tmp) + : [ones]"v"(_mm512_set1_epi16(1)) + ); +# else + __m512i product0 = _mm512_maddubs_epi16(a, b); + product0 = _mm512_madd_epi16(product0, _mm512_set1_epi16(1)); + acc = _mm512_add_epi32(acc, product0); +# endif +# endif + } + + [[maybe_unused]] static void m512_add_dpbusd_epi32x2( + __m512i& acc, + __m512i a0, __m512i b0, + __m512i a1, __m512i b1) { + +# if defined (USE_VNNI) +# if defined (USE_INLINE_ASM) + asm( + "vpdpbusd %[b0], %[a0], %[acc]\n\t" + "vpdpbusd %[b1], %[a1], %[acc]\n\t" + : [acc]"+v"(acc) + : [a0]"v"(a0), [b0]"vm"(b0), [a1]"v"(a1), [b1]"vm"(b1) + ); +# else + acc = _mm512_dpbusd_epi32(acc, a0, b0); + acc = _mm512_dpbusd_epi32(acc, a1, b1); +# endif +# else +# if defined (USE_INLINE_ASM) + __m512i tmp0 = _mm512_maddubs_epi16(a0, b0); + __m512i tmp1 = _mm512_maddubs_epi16(a1, b1); + asm( + "vpaddsw %[tmp0], %[tmp1], %[tmp0]\n\t" + "vpmaddwd %[tmp0], %[ones], %[tmp0]\n\t" + "vpaddd %[acc], %[tmp0], %[acc]\n\t" + : [acc]"+v"(acc), [tmp0]"+&v"(tmp0) + : [tmp1]"v"(tmp1), [ones]"v"(_mm512_set1_epi16(1)) + ); +# else + __m512i product0 = _mm512_maddubs_epi16(a0, b0); + __m512i product1 = _mm512_maddubs_epi16(a1, b1); + product0 = _mm512_adds_epi16(product0, product1); + product0 = _mm512_madd_epi16(product0, _mm512_set1_epi16(1)); + acc = _mm512_add_epi32(acc, product0); +# endif +# endif + } + +#endif + +#if defined (USE_AVX2) + + [[maybe_unused]] static int m256_hadd(__m256i sum, int bias) { + __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(sum), _mm256_extracti128_si256(sum, 1)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_BADC)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_CDAB)); + return _mm_cvtsi128_si32(sum128) + bias; + } + + [[maybe_unused]] static __m128i m256_haddx4( + __m256i sum0, __m256i sum1, __m256i sum2, __m256i sum3, + __m128i bias) { + + sum0 = _mm256_hadd_epi32(sum0, sum1); + sum2 = _mm256_hadd_epi32(sum2, sum3); + + sum0 = _mm256_hadd_epi32(sum0, sum2); + + __m128i sum128lo = _mm256_castsi256_si128(sum0); + __m128i sum128hi = _mm256_extracti128_si256(sum0, 1); + + return _mm_add_epi32(_mm_add_epi32(sum128lo, sum128hi), bias); + } + + [[maybe_unused]] static void m256_add_dpbusd_epi32( + __m256i& acc, + __m256i a, + __m256i b) { + +# if defined (USE_VNNI) +# if defined (USE_INLINE_ASM) + asm( + VNNI_PREFIX "vpdpbusd %[b], %[a], %[acc]\n\t" + : [acc]"+v"(acc) + : [a]"v"(a), [b]"vm"(b) + ); +# else + acc = _mm256_dpbusd_epi32(acc, a, b); +# endif +# else +# if defined (USE_INLINE_ASM) + __m256i tmp = _mm256_maddubs_epi16(a, b); + asm( + "vpmaddwd %[tmp], %[ones], %[tmp]\n\t" + "vpaddd %[acc], %[tmp], %[acc]\n\t" + : [acc]"+v"(acc), [tmp]"+&v"(tmp) + : [ones]"v"(_mm256_set1_epi16(1)) + ); +# else + __m256i product0 = _mm256_maddubs_epi16(a, b); + product0 = _mm256_madd_epi16(product0, _mm256_set1_epi16(1)); + acc = _mm256_add_epi32(acc, product0); +# endif +# endif + } + + [[maybe_unused]] static void m256_add_dpbusd_epi32x2( + __m256i& acc, + __m256i a0, __m256i b0, + __m256i a1, __m256i b1) { + +# if defined (USE_VNNI) +# if defined (USE_INLINE_ASM) + asm( + VNNI_PREFIX "vpdpbusd %[b0], %[a0], %[acc]\n\t" + VNNI_PREFIX "vpdpbusd %[b1], %[a1], %[acc]\n\t" + : [acc]"+v"(acc) + : [a0]"v"(a0), [b0]"vm"(b0), [a1]"v"(a1), [b1]"vm"(b1) + ); +# else + acc = _mm256_dpbusd_epi32(acc, a0, b0); + acc = _mm256_dpbusd_epi32(acc, a1, b1); +# endif +# else +# if defined (USE_INLINE_ASM) + __m256i tmp0 = _mm256_maddubs_epi16(a0, b0); + __m256i tmp1 = _mm256_maddubs_epi16(a1, b1); + asm( + "vpaddsw %[tmp0], %[tmp1], %[tmp0]\n\t" + "vpmaddwd %[tmp0], %[ones], %[tmp0]\n\t" + "vpaddd %[acc], %[tmp0], %[acc]\n\t" + : [acc]"+v"(acc), [tmp0]"+&v"(tmp0) + : [tmp1]"v"(tmp1), [ones]"v"(_mm256_set1_epi16(1)) + ); +# else + __m256i product0 = _mm256_maddubs_epi16(a0, b0); + __m256i product1 = _mm256_maddubs_epi16(a1, b1); + product0 = _mm256_adds_epi16(product0, product1); + product0 = _mm256_madd_epi16(product0, _mm256_set1_epi16(1)); + acc = _mm256_add_epi32(acc, product0); +# endif +# endif + } + +#endif + +#if defined (USE_SSSE3) + + [[maybe_unused]] static int m128_hadd(__m128i sum, int bias) { + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0x4E)); //_MM_PERM_BADC + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0xB1)); //_MM_PERM_CDAB + return _mm_cvtsi128_si32(sum) + bias; + } + + [[maybe_unused]] static __m128i m128_haddx4( + __m128i sum0, __m128i sum1, __m128i sum2, __m128i sum3, + __m128i bias) { + + sum0 = _mm_hadd_epi32(sum0, sum1); + sum2 = _mm_hadd_epi32(sum2, sum3); + sum0 = _mm_hadd_epi32(sum0, sum2); + return _mm_add_epi32(sum0, bias); + } + + [[maybe_unused]] static void m128_add_dpbusd_epi32( + __m128i& acc, + __m128i a, + __m128i b) { + +# if defined (USE_INLINE_ASM) + __m128i tmp = _mm_maddubs_epi16(a, b); + asm( + "pmaddwd %[ones], %[tmp]\n\t" + "paddd %[tmp], %[acc]\n\t" + : [acc]"+v"(acc), [tmp]"+&v"(tmp) + : [ones]"v"(_mm_set1_epi16(1)) + ); +# else + __m128i product0 = _mm_maddubs_epi16(a, b); + product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); + acc = _mm_add_epi32(acc, product0); +# endif + } + + [[maybe_unused]] static void m128_add_dpbusd_epi32x2( + __m128i& acc, + __m128i a0, __m128i b0, + __m128i a1, __m128i b1) { + +# if defined (USE_INLINE_ASM) + __m128i tmp0 = _mm_maddubs_epi16(a0, b0); + __m128i tmp1 = _mm_maddubs_epi16(a1, b1); + asm( + "paddsw %[tmp1], %[tmp0]\n\t" + "pmaddwd %[ones], %[tmp0]\n\t" + "paddd %[tmp0], %[acc]\n\t" + : [acc]"+v"(acc), [tmp0]"+&v"(tmp0) + : [tmp1]"v"(tmp1), [ones]"v"(_mm_set1_epi16(1)) + ); +# else + __m128i product0 = _mm_maddubs_epi16(a0, b0); + __m128i product1 = _mm_maddubs_epi16(a1, b1); + product0 = _mm_adds_epi16(product0, product1); + product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); + acc = _mm_add_epi32(acc, product0); +# endif + } + +#endif + +#if defined (USE_NEON) + + [[maybe_unused]] static int neon_m128_reduce_add_epi32(int32x4_t s) { +# if USE_NEON >= 8 + return vaddvq_s32(s); +# else + return s[0] + s[1] + s[2] + s[3]; +# endif + } + + [[maybe_unused]] static int neon_m128_hadd(int32x4_t sum, int bias) { + return neon_m128_reduce_add_epi32(sum) + bias; + } + + [[maybe_unused]] static int32x4_t neon_m128_haddx4( + int32x4_t sum0, int32x4_t sum1, int32x4_t sum2, int32x4_t sum3, + int32x4_t bias) { + + int32x4_t hsums { + neon_m128_reduce_add_epi32(sum0), + neon_m128_reduce_add_epi32(sum1), + neon_m128_reduce_add_epi32(sum2), + neon_m128_reduce_add_epi32(sum3) + }; + return vaddq_s32(hsums, bias); + } + + [[maybe_unused]] static void neon_m128_add_dpbusd_epi32x2( + int32x4_t& acc, + int8x8_t a0, int8x8_t b0, + int8x8_t a1, int8x8_t b1) { + + int16x8_t product = vmull_s8(a0, b0); + product = vmlal_s8(product, a1, b1); + acc = vpadalq_s16(acc, product); + } + +#endif + +} + +#endif // STOCKFISH_SIMD_H_INCLUDED diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index 721a0ef5ba6..a1315244299 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -1,7 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (c) 2013 Ronald de Man - Copyright (C) 2016-2020 Marco Costalba, Lucas Braesch + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -51,22 +50,23 @@ #include #endif -using namespace Tablebases; +using namespace Stockfish::Tablebases; -int Tablebases::MaxCardinality; +int Stockfish::Tablebases::MaxCardinality; + +namespace Stockfish { namespace { constexpr int TBPIECES = 7; // Max number of supported pieces enum { BigEndian, LittleEndian }; -enum TBType { KEY, WDL, DTZ }; // Used as template parameter +enum TBType { WDL, DTZ }; // Used as template parameter // Each table has a set of flags: all of them refer to DTZ tables, the last one to WDL tables enum TBFlag { STM = 1, Mapped = 2, WinPlies = 4, LossPlies = 8, Wide = 16, SingleValue = 128 }; inline WDLScore operator-(WDLScore d) { return WDLScore(-int(d)); } -inline Square operator^=(Square& s, int i) { return s = Square(int(s) ^ i); } inline Square operator^(Square s, int i) { return Square(int(s) ^ i); } const std::string PieceToChar = " PNBRQK pnbrqk"; @@ -105,9 +105,6 @@ template<> inline void swap_endian(uint8_t&) {} template T number(void* addr) { - static const union { uint32_t i; char c[4]; } Le = { 0x01020304 }; - static const bool IsLittleEndian = (Le.c[0] == 4); - T v; if ((uintptr_t)addr & (alignof(T) - 1)) // Unaligned pointer (very rare) @@ -192,7 +189,8 @@ class TBFile : public std::ifstream { std::stringstream ss(Paths); std::string path; - while (std::getline(ss, path, SepChar)) { + while (std::getline(ss, path, SepChar)) + { fname = path + "/" + f; std::ifstream::open(fname); if (is_open()) @@ -225,7 +223,9 @@ class TBFile : public std::ifstream { *mapping = statbuf.st_size; *baseAddress = mmap(nullptr, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0); +#if defined(MADV_RANDOM) madvise(*baseAddress, statbuf.st_size, MADV_RANDOM); +#endif ::close(fd); if (*baseAddress == MAP_FAILED) @@ -404,7 +404,17 @@ TBTable::TBTable(const TBTable& wdl) : TBTable() { // at init time, accessed at probe time. class TBTables { - typedef std::tuple*, TBTable*> Entry; + struct Entry + { + Key key; + TBTable* wdl; + TBTable* dtz; + + template + TBTable* get() const { + return (TBTable*)(Type == WDL ? (void*)wdl : (void*)dtz); + } + }; static constexpr int Size = 1 << 12; // 4K table, indexed by key's 12 lsb static constexpr int Overflow = 1; // Number of elements allowed to map to the last bucket @@ -416,12 +426,12 @@ class TBTables { void insert(Key key, TBTable* wdl, TBTable* dtz) { uint32_t homeBucket = (uint32_t)key & (Size - 1); - Entry entry = std::make_tuple(key, wdl, dtz); + Entry entry{ key, wdl, dtz }; // Ensure last element is empty to avoid overflow when looking up for (uint32_t bucket = homeBucket; bucket < Size + Overflow - 1; ++bucket) { - Key otherKey = std::get(hashTable[bucket]); - if (otherKey == key || !std::get(hashTable[bucket])) { + Key otherKey = hashTable[bucket].key; + if (otherKey == key || !hashTable[bucket].get()) { hashTable[bucket] = entry; return; } @@ -430,7 +440,7 @@ class TBTables { // insert here and search for a new spot for the other element instead. uint32_t otherHomeBucket = (uint32_t)otherKey & (Size - 1); if (otherHomeBucket > homeBucket) { - swap(entry, hashTable[bucket]); + std::swap(entry, hashTable[bucket]); key = otherKey; homeBucket = otherHomeBucket; } @@ -443,8 +453,8 @@ class TBTables { template TBTable* get(Key key) { for (const Entry* entry = &hashTable[(uint32_t)key & (Size - 1)]; ; ++entry) { - if (std::get(*entry) == key || !std::get(*entry)) - return std::get(*entry); + if (entry->key == key || !entry->get()) + return entry->get(); } } @@ -521,7 +531,7 @@ int decompress_pairs(PairsData* d, uint64_t idx) { // I(k) = k * d->span + d->span / 2 (1) // First step is to get the 'k' of the I(k) nearest to our idx, using definition (1) - uint32_t k = idx / d->span; + uint32_t k = uint32_t(idx / d->span); // Then we read the corresponding SparseIndex[] entry uint32_t block = number(&d->sparseIndex[k].block); @@ -555,7 +565,8 @@ int decompress_pairs(PairsData* d, uint64_t idx) { int buf64Size = 64; Sym sym; - while (true) { + while (true) + { int len = 0; // This is the symbol length - d->min_sym_len // Now get the symbol length. For any symbol s64 of length l right-padded @@ -567,7 +578,7 @@ int decompress_pairs(PairsData* d, uint64_t idx) { // All the symbols of a given length are consecutive integers (numerical // sequence property), so we can compute the offset of our symbol of // length len, stored at the beginning of buf64. - sym = (buf64 - d->base64[len]) >> (64 - len - d->minSymLen); + sym = Sym((buf64 - d->base64[len]) >> (64 - len - d->minSymLen)); // Now add the value of the lowest symbol of length len to get our symbol sym += number(&d->lowestSym[len]); @@ -593,8 +604,8 @@ int decompress_pairs(PairsData* d, uint64_t idx) { // We binary-search for our value recursively expanding into the left and // right child symbols until we reach a leaf node where symlen[sym] + 1 == 1 // that will store the value we need. - while (d->symlen[sym]) { - + while (d->symlen[sym]) + { Sym left = d->btree[sym].get(); // If a symbol contains 36 sub-symbols (d->symlen[sym] + 1 = 36) and @@ -699,14 +710,14 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu leadPawns = b = pos.pieces(color_of(pc), PAWN); do - squares[size++] = pop_lsb(&b) ^ flipSquares; + squares[size++] = pop_lsb(b) ^ flipSquares; while (b); leadPawnsCnt = size; std::swap(squares[0], *std::max_element(squares, squares + leadPawnsCnt, pawns_comp)); - tbFile = map_to_queenside(file_of(squares[0])); + tbFile = File(edge_distance(file_of(squares[0]))); } // DTZ tables are one-sided, i.e. they store positions only for white to @@ -719,7 +730,7 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu // directly map them to the correct color and square. b = pos.pieces() ^ leadPawns; do { - Square s = pop_lsb(&b); + Square s = pop_lsb(b); squares[size] = s ^ flipSquares; pieces[size++] = Piece(pos.piece_on(s) ^ flipColor); } while (b); @@ -743,14 +754,14 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu // the triangle A1-D1-D4. if (file_of(squares[0]) > FILE_D) for (int i = 0; i < size; ++i) - squares[i] ^= 7; // Horizontal flip: SQ_H1 -> SQ_A1 + squares[i] = flip_file(squares[i]); // Encode leading pawns starting with the one with minimum MapPawns[] and // proceeding in ascending order. if (entry->hasPawns) { idx = LeadPawnIdx[leadPawnsCnt][squares[0]]; - std::sort(squares + 1, squares + leadPawnsCnt, pawns_comp); + std::stable_sort(squares + 1, squares + leadPawnsCnt, pawns_comp); for (int i = 1; i < leadPawnsCnt; ++i) idx += Binomial[i][MapPawns[squares[i]]]; @@ -758,11 +769,11 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu goto encode_remaining; // With pawns we have finished special treatments } - // In positions withouth pawns, we further flip the squares to ensure leading + // In positions without pawns, we further flip the squares to ensure leading // piece is below RANK_5. if (rank_of(squares[0]) > RANK_4) for (int i = 0; i < size; ++i) - squares[i] ^= SQ_A8; // Vertical flip: SQ_A8 -> SQ_A1 + squares[i] = flip_rank(squares[i]); // Look for the first piece of the leading group not on the A1-D4 diagonal // and ensure it is mapped below the diagonal. @@ -770,7 +781,7 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu if (!off_A1H8(squares[i])) continue; - if (off_A1H8(squares[i]) > 0) // A1-H8 diagonal flip: SQ_A3 -> SQ_C3 + if (off_A1H8(squares[i]) > 0) // A1-H8 diagonal flip: SQ_A3 -> SQ_C1 for (int j = i; j < size; ++j) squares[j] = Square(((squares[j] >> 3) | (squares[j] << 3)) & 63); break; @@ -801,7 +812,7 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu // Rs "together" in 62 * 61 / 2 ways (we divide by 2 because rooks can be // swapped and still get the same position.) // - // In case we have at least 3 unique pieces (inlcuded kings) we encode them + // In case we have at least 3 unique pieces (included kings) we encode them // together. if (entry->hasUniquePieces) { @@ -816,7 +827,7 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu + (squares[1] - adjust1)) * 62 + squares[2] - adjust2; - // First piece is on a1-h8 diagonal, second below: map this occurence to + // First piece is on a1-h8 diagonal, second below: map this occurrence to // 6 to differentiate from the above case, rank_of() maps a1-d4 diagonal // to 0...3 and finally MapB1H1H7[] maps the b1-h1-h7 triangle to 0..27. else if (off_A1H8(squares[1])) @@ -846,12 +857,12 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu idx *= d->groupIdx[0]; Square* groupSq = squares + d->groupLen[0]; - // Encode remainig pawns then pieces according to square, in ascending order + // Encode remaining pawns then pieces according to square, in ascending order bool remainingPawns = entry->hasPawns && entry->pawnCount[1]; while (d->groupLen[++next]) { - std::sort(groupSq, groupSq + d->groupLen[next]); + std::stable_sort(groupSq, groupSq + d->groupLen[next]); uint64_t n = 0; // Map down a square if "comes later" than a square in the previous @@ -874,7 +885,7 @@ Ret do_probe_table(const Position& pos, T* entry, WDLScore wdl, ProbeState* resu // Group together pieces that will be encoded together. The general rule is that // a group contains pieces of same type and color. The exception is the leading -// group that, in case of positions withouth pawns, can be formed by 3 different +// group that, in case of positions without pawns, can be formed by 3 different // pieces (default) or by the king pair when there is not a unique piece apart // from the kings. When there are pawns, pawns are always first in pieces[]. // @@ -906,7 +917,7 @@ void set_groups(T& e, PairsData* d, int order[], File f) { // // This ensures unique encoding for the whole position. The order of the // groups is a per-table parameter and could not follow the canonical leading - // pawns/pieces -> remainig pawns -> remaining pieces. In particular the + // pawns/pieces -> remaining pawns -> remaining pieces. In particular the // first group is at order[0] position and the remaining pawns, when present, // are at order[1] position. bool pp = e.hasPawns && e.pawnCount[1]; // Pawns on both sides @@ -926,7 +937,7 @@ void set_groups(T& e, PairsData* d, int order[], File f) { d->groupIdx[1] = idx; idx *= Binomial[d->groupLen[1]][48 - d->groupLen[0]]; } - else // Remainig pieces + else // Remaining pieces { d->groupIdx[next] = idx; idx *= Binomial[d->groupLen[next]][freeSquares]; @@ -936,7 +947,7 @@ void set_groups(T& e, PairsData* d, int order[], File f) { d->groupIdx[n] = idx; } -// In Recursive Pairing each symbol represents a pair of childern symbols. So +// In Recursive Pairing each symbol represents a pair of children symbols. So // read d->btree[] symbols data and expand each one in his left and right child // symbol until reaching the leafs that represent the symbol value. uint8_t set_symlen(PairsData* d, Sym s, std::vector& visited) { @@ -975,7 +986,7 @@ uint8_t* set_sizes(PairsData* d, uint8_t* data) { d->sizeofBlock = 1ULL << *data++; d->span = 1ULL << *data++; - d->sparseIndexSize = (tbSize + d->span - 1) / d->span; // Round up + d->sparseIndexSize = size_t((tbSize + d->span - 1) / d->span); // Round up auto padding = number(data++); d->blocksNum = number(data); data += sizeof(uint32_t); d->blockLengthSize = d->blocksNum + padding; // Padded to ensure SparseIndex[] @@ -990,7 +1001,7 @@ uint8_t* set_sizes(PairsData* d, uint8_t* data) { // so that d->lowestSym[i] >= d->lowestSym[i+1] (when read as LittleEndian). // Starting from this we compute a base64[] table indexed by symbol length // and containing 64 bit values so that d->base64[i] >= d->base64[i+1]. - // See http://www.eecs.harvard.edu/~michaelm/E210/huffman.pdf + // See https://en.wikipedia.org/wiki/Huffman_coding for (int i = d->base64.size() - 2; i >= 0; --i) { d->base64[i] = (d->base64[i + 1] + number(&d->lowestSym[i]) - number(&d->lowestSym[i + 1])) / 2; @@ -1131,7 +1142,7 @@ void* mapped(TBTable& e, const Position& pos) { if (e.ready.load(std::memory_order_acquire)) return e.baseAddress; // Could be nullptr if file does not exist - std::unique_lock lk(mutex); + std::scoped_lock lk(mutex); if (e.ready.load(std::memory_order_relaxed)) // Recheck under lock return e.baseAddress; @@ -1191,7 +1202,7 @@ WDLScore search(Position& pos, ProbeState* result) { auto moveList = MoveList(pos); size_t totalCount = moveList.size(), moveCount = 0; - for (const Move& move : moveList) + for (const Move move : moveList) { if ( !pos.capture(move) && (!CheckZeroingMoves || type_of(pos.moved_piece(move)) != PAWN)) @@ -1306,7 +1317,7 @@ void Tablebases::init(const std::string& paths) { for (auto p : bothOnDiagonal) MapKK[p.first][p.second] = code++; - // Binomial[] stores the Binomial Coefficents using Pascal rule. There + // Binomial[] stores the Binomial Coefficients using Pascal rule. There // are Binomial[k][n] ways to choose k elements from a set of n elements. Binomial[0][0] = 1; @@ -1326,7 +1337,7 @@ void Tablebases::init(const std::string& paths) { for (int leadPawnsCnt = 1; leadPawnsCnt <= 5; ++leadPawnsCnt) for (File f = FILE_A; f <= FILE_D; ++f) { - // Restart the index at every file because TB table is splitted + // Restart the index at every file because TB table is split // by file, so we can reuse the same index for different files. int idx = 0; @@ -1344,7 +1355,7 @@ void Tablebases::init(const std::string& paths) { if (leadPawnsCnt == 1) { MapPawns[sq] = availableSquares--; - MapPawns[sq ^ 7] = availableSquares--; // Horizontal flip + MapPawns[flip_file(sq)] = availableSquares--; } LeadPawnIdx[leadPawnsCnt][sq] = idx; idx += Binomial[leadPawnsCnt - 1][MapPawns[sq]]; @@ -1353,7 +1364,7 @@ void Tablebases::init(const std::string& paths) { LeadPawnsSize[leadPawnsCnt][f] = idx; } - // Add entries in TB tables if the corresponding ".rtbw" file exsists + // Add entries in TB tables if the corresponding ".rtbw" file exists for (PieceType p1 = PAWN; p1 < KING; ++p1) { TBTables.add({KING, p1, KING}); @@ -1430,7 +1441,7 @@ WDLScore Tablebases::probe_wdl(Position& pos, ProbeState* result) { // If n = 100 immediately after a capture or pawn move, then the position // is also certainly a win, and during the whole phase until the next // capture or pawn move, the inequality to be preserved is -// dtz + 50-movecounter <= 100. +// dtz + 50-move-counter <= 100. // // In short, if a move is available resulting in dtz + 50-move-counter <= 99, // then do not accept moves leading to dtz + 50-move-counter == 100. @@ -1460,7 +1471,7 @@ int Tablebases::probe_dtz(Position& pos, ProbeState* result) { StateInfo st; int minDTZ = 0xFFFF; - for (const Move& move : MoveList(pos)) + for (const Move move : MoveList(pos)) { bool zeroing = pos.capture(move) || type_of(pos.moved_piece(move)) == PAWN; @@ -1525,6 +1536,14 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) { WDLScore wdl = -probe_wdl(pos, &result); dtz = dtz_before_zeroing(wdl); } + else if (pos.is_draw(1)) + { + // In case a root move leads to a draw by repetition or + // 50-move rule, we set dtz to zero. Note: since we are + // only 1 ply from the root, this must be a true 3-fold + // repetition inside the game history. + dtz = 0; + } else { // Otherwise, take dtz for the new position and correct by 1 ply @@ -1575,6 +1594,7 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) { ProbeState result; StateInfo st; + WDLScore wdl; bool rule50 = Options["Syzygy50MoveRule"]; @@ -1583,7 +1603,10 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) { { pos.do_move(m.pv[0], st); - WDLScore wdl = -probe_wdl(pos, &result); + if (pos.is_draw(1)) + wdl = WDLDraw; + else + wdl = -probe_wdl(pos, &result); pos.undo_move(m.pv[0]); @@ -1600,3 +1623,5 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) { return true; } + +} // namespace Stockfish diff --git a/src/syzygy/tbprobe.h b/src/syzygy/tbprobe.h index df3ca4feccc..c2917fef636 100644 --- a/src/syzygy/tbprobe.h +++ b/src/syzygy/tbprobe.h @@ -1,7 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (c) 2013 Ronald de Man - Copyright (C) 2016-2020 Marco Costalba, Lucas Braesch + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,7 +23,7 @@ #include "../search.h" -namespace Tablebases { +namespace Stockfish::Tablebases { enum WDLScore { WDLLoss = -2, // Loss @@ -39,7 +38,7 @@ enum WDLScore { // Possible states after a probing operation enum ProbeState { FAIL = 0, // Probe failed (missing file table) - OK = 1, // Probe succesful + OK = 1, // Probe successful CHANGE_STM = -1, // DTZ should check the other side ZEROING_BEST_MOVE = 2 // Best move zeroes DTZ (capture or pawn move) }; @@ -74,6 +73,6 @@ inline std::ostream& operator<<(std::ostream& os, const ProbeState v) { return os; } -} +} // namespace Stockfish::Tablebases #endif diff --git a/src/thread.cpp b/src/thread.cpp index 615d482cafa..30177a3915a 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,6 +26,8 @@ #include "syzygy/tbprobe.h" #include "tt.h" +namespace Stockfish { + ThreadPool Threads; // Global object @@ -52,15 +52,6 @@ Thread::~Thread() { stdThread.join(); } -/// Thread::bestMoveCount(Move move) return best move counter for the given root move - -int Thread::best_move_count(Move move) { - - auto rm = std::find(rootMoves.begin() + pvIdx, - rootMoves.begin() + pvLast, move); - - return rm != rootMoves.begin() + pvLast ? rm->bestMoveCount : 0; -} /// Thread::clear() reset histories, usually before a new game @@ -71,16 +62,16 @@ void Thread::clear() { captureHistory.fill(0); for (bool inCheck : { false, true }) - for (StatsType c : { NoCaptures, Captures }) - for (auto& to : continuationHistory[inCheck][c]) - for (auto& h : to) - h->fill(0); - - for (bool inCheck : { false, true }) - for (StatsType c : { NoCaptures, Captures }) - continuationHistory[inCheck][c][NO_PIECE][0]->fill(Search::CounterMovePruneThreshold - 1); + for (StatsType c : { NoCaptures, Captures }) + { + for (auto& to : continuationHistory[inCheck][c]) + for (auto& h : to) + h->fill(-71); + continuationHistory[inCheck][c][NO_PIECE][0]->fill(Search::CounterMovePruneThreshold - 1); + } } + /// Thread::start_searching() wakes up the thread that will start the search void Thread::start_searching() { @@ -136,14 +127,16 @@ void Thread::idle_loop() { void ThreadPool::set(size_t requested) { - if (size() > 0) { // destroy any existing thread(s) + if (size() > 0) // destroy any existing thread(s) + { main()->wait_for_search_finished(); while (size() > 0) delete back(), pop_back(); } - if (requested > 0) { // create new thread(s) + if (requested > 0) // create new thread(s) + { push_back(new MainThread(0)); while (size() < requested) @@ -151,14 +144,15 @@ void ThreadPool::set(size_t requested) { clear(); // Reallocate the hash with the new threadpool size - TT.resize(Options["Hash"]); + TT.resize(size_t(Options["Hash"])); // Init thread number dependent search params. Search::init(); } } -/// ThreadPool::clear() sets threadPool data to initial values. + +/// ThreadPool::clear() sets threadPool data to initial values void ThreadPool::clear() { @@ -166,10 +160,12 @@ void ThreadPool::clear() { th->clear(); main()->callsCnt = 0; - main()->previousScore = VALUE_INFINITE; + main()->bestPreviousScore = VALUE_INFINITE; + main()->bestPreviousAverageScore = VALUE_INFINITE; main()->previousTimeReduction = 1.0; } + /// ThreadPool::start_thinking() wakes up main thread waiting in idle_loop() and /// returns immediately. Main thread will wake up other threads and start the search. @@ -201,20 +197,70 @@ void ThreadPool::start_thinking(Position& pos, StateListPtr& states, // We use Position::set() to set root position across threads. But there are // some StateInfo fields (previous, pliesFromNull, capturedPiece) that cannot - // be deduced from a fen string, so set() clears them and to not lose the info - // we need to backup and later restore setupStates->back(). Note that setupStates - // is shared by threads but is accessed in read-only mode. - StateInfo tmp = setupStates->back(); - + // be deduced from a fen string, so set() clears them and they are set from + // setupStates->back() later. The rootState is per thread, earlier states are shared + // since they are read-only. for (Thread* th : *this) { - th->nodes = th->tbHits = th->nmpMinPly = 0; + th->nodes = th->tbHits = th->nmpMinPly = th->bestMoveChanges = 0; th->rootDepth = th->completedDepth = 0; th->rootMoves = rootMoves; - th->rootPos.set(pos.fen(), pos.is_chess960(), &setupStates->back(), th); + th->rootPos.set(pos.fen(), pos.is_chess960(), &th->rootState, th); + th->rootState = setupStates->back(); } - setupStates->back() = tmp; - main()->start_searching(); } + +Thread* ThreadPool::get_best_thread() const { + + Thread* bestThread = front(); + std::map votes; + Value minScore = VALUE_NONE; + + // Find minimum score of all threads + for (Thread* th: *this) + minScore = std::min(minScore, th->rootMoves[0].score); + + // Vote according to score and depth, and select the best thread + for (Thread* th : *this) + { + votes[th->rootMoves[0].pv[0]] += + (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth); + + if (abs(bestThread->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY) + { + // Make sure we pick the shortest mate / TB conversion or stave off mate the longest + if (th->rootMoves[0].score > bestThread->rootMoves[0].score) + bestThread = th; + } + else if ( th->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY + || ( th->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY + && votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]])) + bestThread = th; + } + + return bestThread; +} + + +/// Start non-main threads + +void ThreadPool::start_searching() { + + for (Thread* th : *this) + if (th != front()) + th->start_searching(); +} + + +/// Wait for non-main threads + +void ThreadPool::wait_for_search_finished() const { + + for (Thread* th : *this) + if (th != front()) + th->wait_for_search_finished(); +} + +} // namespace Stockfish diff --git a/src/thread.h b/src/thread.h index aea86fd5c69..c3d38f3cf31 100644 --- a/src/thread.h +++ b/src/thread.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -34,6 +32,7 @@ #include "search.h" #include "thread_win32_osx.h" +namespace Stockfish { /// Thread class keeps together all the thread-related stuff. We use /// per-thread pawn and material hash tables so that once we get a @@ -56,24 +55,32 @@ class Thread { void idle_loop(); void start_searching(); void wait_for_search_finished(); - int best_move_count(Move move); + size_t id() const { return idx; } Pawns::Table pawnsTable; Material::Table materialTable; size_t pvIdx, pvLast; - uint64_t ttHitAverage; + RunningAverage doubleExtensionAverage[COLOR_NB]; + RunningAverage complexityAverage; + uint64_t nodesLastExplosive; + uint64_t nodesLastNormal; + std::atomic nodes, tbHits, bestMoveChanges; + Value bestValue; int selDepth, nmpMinPly; Color nmpColor; - std::atomic nodes, tbHits, bestMoveChanges; + ExplosionState state; + Value optimism[COLOR_NB]; Position rootPos; + StateInfo rootState; Search::RootMoves rootMoves; Depth rootDepth, completedDepth; + Value rootDelta; CounterMoveHistory counterMoves; ButterflyHistory mainHistory; CapturePieceToHistory captureHistory; ContinuationHistory continuationHistory[2][2]; - Score contempt; + Score trend; }; @@ -87,7 +94,8 @@ struct MainThread : public Thread { void check_time(); double previousTimeReduction; - Value previousScore; + Value bestPreviousScore; + Value bestPreviousAverageScore; Value iterValue[4]; int callsCnt; bool stopOnPonderhit; @@ -108,6 +116,9 @@ struct ThreadPool : public std::vector { MainThread* main() const { return static_cast(front()); } uint64_t nodes_searched() const { return accumulate(&Thread::nodes); } uint64_t tb_hits() const { return accumulate(&Thread::tbHits); } + Thread* get_best_thread() const; + void start_searching(); + void wait_for_search_finished() const; std::atomic_bool stop, increaseDepth; @@ -125,4 +136,6 @@ struct ThreadPool : public std::vector { extern ThreadPool Threads; +} // namespace Stockfish + #endif // #ifndef THREAD_H_INCLUDED diff --git a/src/thread_win32_osx.h b/src/thread_win32_osx.h index 0ef5c98132a..77d1c3c7ef3 100644 --- a/src/thread_win32_osx.h +++ b/src/thread_win32_osx.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -29,10 +27,12 @@ /// The implementation calls pthread_create() with the stack size parameter /// equal to the linux 8MB default, on platforms that support it. -#if defined(__APPLE__) || defined(__MINGW32__) || defined(__MINGW64__) +#if defined(__APPLE__) || defined(__MINGW32__) || defined(__MINGW64__) || defined(USE_PTHREADS) #include +namespace Stockfish { + static const size_t TH_STACK_SIZE = 8 * 1024 * 1024; template > @@ -59,10 +59,16 @@ class NativeThread { void join() { pthread_join(thread, NULL); } }; +} // namespace Stockfish + #else // Default case: use STL classes +namespace Stockfish { + typedef std::thread NativeThread; +} // namespace Stockfish + #endif #endif // #ifndef THREAD_WIN32_OSX_H_INCLUDED diff --git a/src/timeman.cpp b/src/timeman.cpp index 0848be420c0..0400401e779 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,68 +24,25 @@ #include "timeman.h" #include "uci.h" -TimeManagement Time; // Our global time management object - -namespace { - - enum TimeType { OptimumTime, MaxTime }; - - constexpr int MoveHorizon = 50; // Plan time management at most this many moves ahead - constexpr double MaxRatio = 7.3; // When in trouble, we can step over reserved time with this ratio - constexpr double StealRatio = 0.34; // However we must not steal time from remaining moves over this ratio - - - // move_importance() is a skew-logistic function based on naive statistical - // analysis of "how many games are still undecided after n half-moves". Game - // is considered "undecided" as long as neither side has >275cp advantage. - // Data was extracted from the CCRL game database with some simple filtering criteria. - - double move_importance(int ply) { - - constexpr double XScale = 6.85; - constexpr double XShift = 64.5; - constexpr double Skew = 0.171; - - return pow((1 + exp((ply - XShift) / XScale)), -Skew) + DBL_MIN; // Ensure non-zero - } - - template - TimePoint remaining(TimePoint myTime, int movesToGo, int ply, TimePoint slowMover) { - - constexpr double TMaxRatio = (T == OptimumTime ? 1.0 : MaxRatio); - constexpr double TStealRatio = (T == OptimumTime ? 0.0 : StealRatio); - - double moveImportance = (move_importance(ply) * slowMover) / 100.0; - double otherMovesImportance = 0.0; +namespace Stockfish { - for (int i = 1; i < movesToGo; ++i) - otherMovesImportance += move_importance(ply + 2 * i); - - double ratio1 = (TMaxRatio * moveImportance) / (TMaxRatio * moveImportance + otherMovesImportance); - double ratio2 = (moveImportance + TStealRatio * otherMovesImportance) / (moveImportance + otherMovesImportance); - - return TimePoint(myTime * std::min(ratio1, ratio2)); // Intel C++ asks for an explicit cast - } - -} // namespace +TimeManagement Time; // Our global time management object -/// init() is called at the beginning of the search and calculates the allowed -/// thinking time out of the time control and current game ply. We support four -/// different kinds of time controls, passed in 'limits': -/// -/// inc == 0 && movestogo == 0 means: x basetime [sudden death!] -/// inc == 0 && movestogo != 0 means: x moves in y minutes -/// inc > 0 && movestogo == 0 means: x basetime + z increment -/// inc > 0 && movestogo != 0 means: x moves in y minutes + z increment +/// TimeManagement::init() is called at the beginning of the search and calculates +/// the bounds of time allowed for the current game ply. We currently support: +// 1) x basetime (+ z increment) +// 2) x moves in y seconds (+ z increment) void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) { - TimePoint minThinkingTime = Options["Minimum Thinking Time"]; - TimePoint moveOverhead = Options["Move Overhead"]; - TimePoint slowMover = Options["Slow Mover"]; - TimePoint npmsec = Options["nodestime"]; - TimePoint hypMyTime; + TimePoint moveOverhead = TimePoint(Options["Move Overhead"]); + TimePoint slowMover = TimePoint(Options["Slow Mover"]); + TimePoint npmsec = TimePoint(Options["nodestime"]); + + // optScale is a percentage of available time to use for the current move. + // maxScale is a multiplier applied to optimumTime. + double optScale, maxScale; // If we have to play in 'nodes as time' mode, then convert from time // to nodes, and use resulting values in time management formulas. @@ -105,29 +60,46 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) { } startTime = limits.startTime; - optimumTime = maximumTime = std::max(limits.time[us], minThinkingTime); - const int maxMTG = limits.movestogo ? std::min(limits.movestogo, MoveHorizon) : MoveHorizon; + // Maximum move horizon of 50 moves + int mtg = limits.movestogo ? std::min(limits.movestogo, 50) : 50; - // We calculate optimum time usage for different hypothetical "moves to go" values - // and choose the minimum of calculated search time values. Usually the greatest - // hypMTG gives the minimum values. - for (int hypMTG = 1; hypMTG <= maxMTG; ++hypMTG) - { - // Calculate thinking time for hypothetical "moves to go"-value - hypMyTime = limits.time[us] - + limits.inc[us] * (hypMTG - 1) - - moveOverhead * (2 + std::min(hypMTG, 40)); + // Make sure timeLeft is > 0 since we may use it as a divisor + TimePoint timeLeft = std::max(TimePoint(1), + limits.time[us] + limits.inc[us] * (mtg - 1) - moveOverhead * (2 + mtg)); - hypMyTime = std::max(hypMyTime, TimePoint(0)); + // Use extra time with larger increments + double optExtra = std::clamp(1.0 + 12.0 * limits.inc[us] / limits.time[us], 1.0, 1.12); - TimePoint t1 = minThinkingTime + remaining(hypMyTime, hypMTG, ply, slowMover); - TimePoint t2 = minThinkingTime + remaining(hypMyTime, hypMTG, ply, slowMover); + // A user may scale time usage by setting UCI option "Slow Mover" + // Default is 100 and changing this value will probably lose elo. + timeLeft = slowMover * timeLeft / 100; - optimumTime = std::min(t1, optimumTime); - maximumTime = std::min(t2, maximumTime); + // x basetime (+ z increment) + // If there is a healthy increment, timeLeft can exceed actual available + // game time for the current move, so also cap to 20% of available game time. + if (limits.movestogo == 0) + { + optScale = std::min(0.0084 + std::pow(ply + 3.0, 0.5) * 0.0042, + 0.2 * limits.time[us] / double(timeLeft)) + * optExtra; + maxScale = std::min(7.0, 4.0 + ply / 12.0); + } + + // x moves in y seconds (+ z increment) + else + { + optScale = std::min((0.88 + ply / 116.4) / mtg, + 0.88 * limits.time[us] / double(timeLeft)); + maxScale = std::min(6.3, 1.5 + 0.11 * mtg); } + // Never use more than 80% of the available time for this move + optimumTime = TimePoint(optScale * timeLeft); + maximumTime = TimePoint(std::min(0.8 * limits.time[us] - moveOverhead, maxScale * optimumTime)); + if (Options["Ponder"]) optimumTime += optimumTime / 4; } + +} // namespace Stockfish diff --git a/src/timeman.h b/src/timeman.h index 9301dc946f7..a86f07693a0 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,6 +23,8 @@ #include "search.h" #include "thread.h" +namespace Stockfish { + /// The TimeManagement class computes the optimal time to think depending on /// the maximum available time, the game move number and other parameters. @@ -46,4 +46,6 @@ class TimeManagement { extern TimeManagement Time; +} // namespace Stockfish + #endif // #ifndef TIMEMAN_H_INCLUDED diff --git a/src/tt.cpp b/src/tt.cpp index 0b4a59de559..c7118aea3fc 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,29 +26,32 @@ #include "tt.h" #include "uci.h" +namespace Stockfish { + TranspositionTable TT; // Our global transposition table -/// TTEntry::save populates the TTEntry with a new node's data, possibly +/// TTEntry::save() populates the TTEntry with a new node's data, possibly /// overwriting an old position. Update is not atomic and can be racy. void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev) { // Preserve any existing move for the same position - if (m || (k >> 48) != key16) + if (m || (uint16_t)k != key16) move16 = (uint16_t)m; - // Overwrite less valuable entries - if ( (k >> 48) != key16 - || d - DEPTH_OFFSET > depth8 - 4 - || b == BOUND_EXACT) + // Overwrite less valuable entries (cheapest checks first) + if ( b == BOUND_EXACT + || (uint16_t)k != key16 + || d - DEPTH_OFFSET + 2 * pv > depth8 - 4) { - assert(d >= DEPTH_OFFSET); + assert(d > DEPTH_OFFSET); + assert(d < 256 + DEPTH_OFFSET); - key16 = (uint16_t)(k >> 48); + key16 = (uint16_t)k; + depth8 = (uint8_t)(d - DEPTH_OFFSET); + genBound8 = (uint8_t)(TT.generation8 | uint8_t(pv) << 2 | b); value16 = (int16_t)v; eval16 = (int16_t)ev; - genBound8 = (uint8_t)(TT.generation8 | uint8_t(pv) << 2 | b); - depth8 = (uint8_t)(d - DEPTH_OFFSET); } } @@ -63,19 +64,18 @@ void TranspositionTable::resize(size_t mbSize) { Threads.main()->wait_for_search_finished(); - clusterCount = mbSize * 1024 * 1024 / sizeof(Cluster); + aligned_large_pages_free(table); - free(mem); - mem = malloc(clusterCount * sizeof(Cluster) + CacheLineSize - 1); + clusterCount = mbSize * 1024 * 1024 / sizeof(Cluster); - if (!mem) + table = static_cast(aligned_large_pages_alloc(clusterCount * sizeof(Cluster))); + if (!table) { std::cerr << "Failed to allocate " << mbSize << "MB for transposition table." << std::endl; exit(EXIT_FAILURE); } - table = (Cluster*)((uintptr_t(mem) + CacheLineSize - 1) & ~(CacheLineSize - 1)); clear(); } @@ -96,8 +96,8 @@ void TranspositionTable::clear() { WinProcGroup::bindThisThread(idx); // Each thread will zero its part of the hash table - const size_t stride = clusterCount / Options["Threads"], - start = stride * idx, + const size_t stride = size_t(clusterCount / Options["Threads"]), + start = size_t(stride * idx), len = idx != Options["Threads"] - 1 ? stride : clusterCount - start; @@ -105,10 +105,11 @@ void TranspositionTable::clear() { }); } - for (std::thread& th: threads) + for (std::thread& th : threads) th.join(); } + /// TranspositionTable::probe() looks up the current position in the transposition /// table. It returns true and a pointer to the TTEntry if the position is found. /// Otherwise, it returns false and a pointer to an empty or least valuable TTEntry @@ -119,25 +120,26 @@ void TranspositionTable::clear() { TTEntry* TranspositionTable::probe(const Key key, bool& found) const { TTEntry* const tte = first_entry(key); - const uint16_t key16 = key >> 48; // Use the high 16 bits as key inside the cluster + const uint16_t key16 = (uint16_t)key; // Use the low 16 bits as key inside the cluster for (int i = 0; i < ClusterSize; ++i) - if (!tte[i].key16 || tte[i].key16 == key16) + if (tte[i].key16 == key16 || !tte[i].depth8) { - tte[i].genBound8 = uint8_t(generation8 | (tte[i].genBound8 & 0x7)); // Refresh + tte[i].genBound8 = uint8_t(generation8 | (tte[i].genBound8 & (GENERATION_DELTA - 1))); // Refresh - return found = (bool)tte[i].key16, &tte[i]; + return found = (bool)tte[i].depth8, &tte[i]; } // Find an entry to be replaced according to the replacement strategy TTEntry* replace = tte; for (int i = 1; i < ClusterSize; ++i) // Due to our packed storage format for generation and its cyclic - // nature we add 263 (256 is the modulus plus 7 to keep the unrelated - // lowest three bits from affecting the result) to calculate the entry - // age correctly even after generation8 overflows into the next cycle. - if ( replace->depth8 - ((263 + generation8 - replace->genBound8) & 0xF8) - > tte[i].depth8 - ((263 + generation8 - tte[i].genBound8) & 0xF8)) + // nature we add GENERATION_CYCLE (256 is the modulus, plus what + // is needed to keep the unrelated lowest n bits from affecting + // the result) to calculate the entry age correctly even after + // generation8 overflows into the next cycle. + if ( replace->depth8 - ((GENERATION_CYCLE + generation8 - replace->genBound8) & GENERATION_MASK) + > tte[i].depth8 - ((GENERATION_CYCLE + generation8 - tte[i].genBound8) & GENERATION_MASK)) replace = &tte[i]; return found = false, replace; @@ -150,9 +152,11 @@ TTEntry* TranspositionTable::probe(const Key key, bool& found) const { int TranspositionTable::hashfull() const { int cnt = 0; - for (int i = 0; i < 1000 / ClusterSize; ++i) + for (int i = 0; i < 1000; ++i) for (int j = 0; j < ClusterSize; ++j) - cnt += (table[i].entry[j].genBound8 & 0xF8) == generation8; + cnt += table[i].entry[j].depth8 && (table[i].entry[j].genBound8 & GENERATION_MASK) == generation8; - return cnt * 1000 / (ClusterSize * (1000 / ClusterSize)); + return cnt / ClusterSize; } + +} // namespace Stockfish diff --git a/src/tt.h b/src/tt.h index 98b054d3972..03fe3e143d1 100644 --- a/src/tt.h +++ b/src/tt.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,16 +22,18 @@ #include "misc.h" #include "types.h" +namespace Stockfish { + /// TTEntry struct is the 10 bytes transposition table entry, defined as below: /// /// key 16 bit -/// move 16 bit -/// value 16 bit -/// eval value 16 bit +/// depth 8 bit /// generation 5 bit /// pv node 1 bit /// bound type 2 bit -/// depth 8 bit +/// move 16 bit +/// value 16 bit +/// eval value 16 bit struct TTEntry { @@ -41,7 +41,7 @@ struct TTEntry { Value value() const { return (Value)value16; } Value eval() const { return (Value)eval16; } Depth depth() const { return (Depth)depth8 + DEPTH_OFFSET; } - bool is_pv() const { return (bool)(genBound8 & 0x4); } + bool is_pv() const { return (bool)(genBound8 & 0x4); } Bound bound() const { return (Bound)(genBound8 & 0x3); } void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev); @@ -49,44 +49,47 @@ struct TTEntry { friend class TranspositionTable; uint16_t key16; + uint8_t depth8; + uint8_t genBound8; uint16_t move16; int16_t value16; int16_t eval16; - uint8_t genBound8; - uint8_t depth8; }; -/// A TranspositionTable consists of a power of 2 number of clusters and each -/// cluster consists of ClusterSize number of TTEntry. Each non-empty entry -/// contains information of exactly one position. The size of a cluster should -/// divide the size of a cache line size, to ensure that clusters never cross -/// cache lines. This ensures best cache performance, as the cacheline is -/// prefetched, as soon as possible. +/// A TranspositionTable is an array of Cluster, of size clusterCount. Each +/// cluster consists of ClusterSize number of TTEntry. Each non-empty TTEntry +/// contains information on exactly one position. The size of a Cluster should +/// divide the size of a cache line for best performance, as the cacheline is +/// prefetched when possible. class TranspositionTable { - static constexpr int CacheLineSize = 64; static constexpr int ClusterSize = 3; struct Cluster { TTEntry entry[ClusterSize]; - char padding[2]; // Align to a divisor of the cache line size + char padding[2]; // Pad to 32 bytes }; - static_assert(CacheLineSize % sizeof(Cluster) == 0, "Cluster size incorrect"); + static_assert(sizeof(Cluster) == 32, "Unexpected Cluster size"); + + // Constants used to refresh the hash table periodically + static constexpr unsigned GENERATION_BITS = 3; // nb of bits reserved for other things + static constexpr int GENERATION_DELTA = (1 << GENERATION_BITS); // increment for generation field + static constexpr int GENERATION_CYCLE = 255 + (1 << GENERATION_BITS); // cycle length + static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; // mask to pull out generation number public: - ~TranspositionTable() { free(mem); } - void new_search() { generation8 += 8; } // Lower 3 bits are used by PV flag and Bound + ~TranspositionTable() { aligned_large_pages_free(table); } + void new_search() { generation8 += GENERATION_DELTA; } // Lower bits are used for other things TTEntry* probe(const Key key, bool& found) const; int hashfull() const; void resize(size_t mbSize); void clear(); - // The 32 lowest order bits of the key are used to get the index of the cluster TTEntry* first_entry(const Key key) const { - return &table[(uint32_t(key) * uint64_t(clusterCount)) >> 32].entry[0]; + return &table[mul_hi64(key, clusterCount)].entry[0]; } private: @@ -94,10 +97,11 @@ class TranspositionTable { size_t clusterCount; Cluster* table; - void* mem; uint8_t generation8; // Size must be not bigger than TTEntry::genBound8 }; extern TranspositionTable TT; +} // namespace Stockfish + #endif // #ifndef TT_H_INCLUDED diff --git a/src/tune.cpp b/src/tune.cpp new file mode 100644 index 00000000000..a885845f750 --- /dev/null +++ b/src/tune.cpp @@ -0,0 +1,133 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "types.h" +#include "misc.h" +#include "uci.h" + +using std::string; + +namespace Stockfish { + +bool Tune::update_on_last; +const UCI::Option* LastOption = nullptr; +static std::map TuneResults; + +string Tune::next(string& names, bool pop) { + + string name; + + do { + string token = names.substr(0, names.find(',')); + + if (pop) + names.erase(0, token.size() + 1); + + std::stringstream ws(token); + name += (ws >> token, token); // Remove trailing whitespace + + } while ( std::count(name.begin(), name.end(), '(') + - std::count(name.begin(), name.end(), ')')); + + return name; +} + +static void on_tune(const UCI::Option& o) { + + if (!Tune::update_on_last || LastOption == &o) + Tune::read_options(); +} + +static void make_option(const string& n, int v, const SetRange& r) { + + // Do not generate option when there is nothing to tune (ie. min = max) + if (r(v).first == r(v).second) + return; + + if (TuneResults.count(n)) + v = TuneResults[n]; + + Options[n] << UCI::Option(v, r(v).first, r(v).second, on_tune); + LastOption = &Options[n]; + + // Print formatted parameters, ready to be copy-pasted in Fishtest + std::cout << n << "," + << v << "," + << r(v).first << "," << r(v).second << "," + << (r(v).second - r(v).first) / 20.0 << "," + << "0.0020" + << std::endl; +} + +template<> void Tune::Entry::init_option() { make_option(name, value, range); } + +template<> void Tune::Entry::read_option() { + if (Options.count(name)) + value = int(Options[name]); +} + +template<> void Tune::Entry::init_option() { make_option(name, value, range); } + +template<> void Tune::Entry::read_option() { + if (Options.count(name)) + value = Value(int(Options[name])); +} + +template<> void Tune::Entry::init_option() { + make_option("m" + name, mg_value(value), range); + make_option("e" + name, eg_value(value), range); +} + +template<> void Tune::Entry::read_option() { + if (Options.count("m" + name)) + value = make_score(int(Options["m" + name]), eg_value(value)); + + if (Options.count("e" + name)) + value = make_score(mg_value(value), int(Options["e" + name])); +} + +// Instead of a variable here we have a PostUpdate function: just call it +template<> void Tune::Entry::init_option() {} +template<> void Tune::Entry::read_option() { value(); } + +} // namespace Stockfish + + +// Init options with tuning session results instead of default values. Useful to +// get correct bench signature after a tuning session or to test tuned values. +// Just copy fishtest tuning results in a result.txt file and extract the +// values with: +// +// cat results.txt | sed 's/^param: \([^,]*\), best: \([^,]*\).*/ TuneResults["\1"] = int(round(\2));/' +// +// Then paste the output below, as the function body + +#include + +namespace Stockfish { + +void Tune::read_results() { + + /* ...insert your values here... */ +} + +} // namespace Stockfish diff --git a/src/tune.h b/src/tune.h new file mode 100644 index 00000000000..75ab484acf0 --- /dev/null +++ b/src/tune.h @@ -0,0 +1,163 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef TUNE_H_INCLUDED +#define TUNE_H_INCLUDED + +#include +#include +#include +#include + +namespace Stockfish { + +typedef std::pair Range; // Option's min-max values +typedef Range (RangeFun) (int); + +// Default Range function, to calculate Option's min-max values +inline Range default_range(int v) { + return v > 0 ? Range(0, 2 * v) : Range(2 * v, 0); +} + +struct SetRange { + explicit SetRange(RangeFun f) : fun(f) {} + SetRange(int min, int max) : fun(nullptr), range(min, max) {} + Range operator()(int v) const { return fun ? fun(v) : range; } + + RangeFun* fun; + Range range; +}; + +#define SetDefaultRange SetRange(default_range) + + +/// Tune class implements the 'magic' code that makes the setup of a fishtest +/// tuning session as easy as it can be. Mainly you have just to remove const +/// qualifiers from the variables you want to tune and flag them for tuning, so +/// if you have: +/// +/// const Score myScore = S(10, 15); +/// const Value myValue[][2] = { { V(100), V(20) }, { V(7), V(78) } }; +/// +/// If you have a my_post_update() function to run after values have been updated, +/// and a my_range() function to set custom Option's min-max values, then you just +/// remove the 'const' qualifiers and write somewhere below in the file: +/// +/// TUNE(SetRange(my_range), myScore, myValue, my_post_update); +/// +/// You can also set the range directly, and restore the default at the end +/// +/// TUNE(SetRange(-100, 100), myScore, SetDefaultRange); +/// +/// In case update function is slow and you have many parameters, you can add: +/// +/// UPDATE_ON_LAST(); +/// +/// And the values update, including post update function call, will be done only +/// once, after the engine receives the last UCI option, that is the one defined +/// and created as the last one, so the GUI should send the options in the same +/// order in which have been defined. + +class Tune { + + typedef void (PostUpdate) (); // Post-update function + + Tune() { read_results(); } + Tune(const Tune&) = delete; + void operator=(const Tune&) = delete; + void read_results(); + + static Tune& instance() { static Tune t; return t; } // Singleton + + // Use polymorphism to accommodate Entry of different types in the same vector + struct EntryBase { + virtual ~EntryBase() = default; + virtual void init_option() = 0; + virtual void read_option() = 0; + }; + + template + struct Entry : public EntryBase { + + static_assert(!std::is_const::value, "Parameter cannot be const!"); + + static_assert( std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value, "Parameter type not supported!"); + + Entry(const std::string& n, T& v, const SetRange& r) : name(n), value(v), range(r) {} + void operator=(const Entry&) = delete; // Because 'value' is a reference + void init_option() override; + void read_option() override; + + std::string name; + T& value; + SetRange range; + }; + + // Our facility to fill the container, each Entry corresponds to a parameter + // to tune. We use variadic templates to deal with an unspecified number of + // entries, each one of a possible different type. + static std::string next(std::string& names, bool pop = true); + + int add(const SetRange&, std::string&&) { return 0; } + + template + int add(const SetRange& range, std::string&& names, T& value, Args&&... args) { + list.push_back(std::unique_ptr(new Entry(next(names), value, range))); + return add(range, std::move(names), args...); + } + + // Template specialization for arrays: recursively handle multi-dimensional arrays + template + int add(const SetRange& range, std::string&& names, T (&value)[N], Args&&... args) { + for (size_t i = 0; i < N; i++) + add(range, next(names, i == N - 1) + "[" + std::to_string(i) + "]", value[i]); + return add(range, std::move(names), args...); + } + + // Template specialization for SetRange + template + int add(const SetRange&, std::string&& names, SetRange& value, Args&&... args) { + return add(value, (next(names), std::move(names)), args...); + } + + std::vector> list; + +public: + template + static int add(const std::string& names, Args&&... args) { + return instance().add(SetDefaultRange, names.substr(1, names.size() - 2), args...); // Remove trailing parenthesis + } + static void init() { for (auto& e : instance().list) e->init_option(); read_options(); } // Deferred, due to UCI::Options access + static void read_options() { for (auto& e : instance().list) e->read_option(); } + static bool update_on_last; +}; + +// Some macro magic :-) we define a dummy int variable that compiler initializes calling Tune::add() +#define STRINGIFY(x) #x +#define UNIQUE2(x, y) x ## y +#define UNIQUE(x, y) UNIQUE2(x, y) // Two indirection levels to expand __LINE__ +#define TUNE(...) int UNIQUE(p, __LINE__) = Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__) + +#define UPDATE_ON_LAST() bool UNIQUE(p, __LINE__) = Tune::update_on_last = true + +} // namespace Stockfish + +#endif // #ifndef TUNE_H_INCLUDED diff --git a/src/types.h b/src/types.h index 902c2cfce87..a3a873fa4e7 100644 --- a/src/types.h +++ b/src/types.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -40,7 +38,6 @@ #include #include -#include #include #include #include @@ -60,6 +57,12 @@ /// _WIN32 Building on Windows (any) /// _WIN64 Building on Windows 64 bit +#if defined(__GNUC__ ) && (__GNUC__ < 9 || (__GNUC__ == 9 && __GNUC_MINOR__ <= 2)) && defined(_WIN32) && !defined(__clang__) +#define ALIGNAS_ON_STACK_VARIABLES_BROKEN +#endif + +#define ASSERT_ALIGNED(ptr, alignment) assert(reinterpret_cast(ptr) % alignment == 0) + #if defined(_WIN64) && defined(_MSC_VER) // No Makefile used # include // Microsoft header for _BitScanForward64() # define IS_64BIT @@ -80,6 +83,8 @@ # define pext(b, m) 0 #endif +namespace Stockfish { + #ifdef USE_POPCNT constexpr bool HasPopCnt = true; #else @@ -110,7 +115,7 @@ constexpr int MAX_PLY = 246; /// bit 6-11: origin square (from 0 to 63) /// bit 12-13: promotion piece type - 2 (from KNIGHT-2 to QUEEN-2) /// bit 14-15: special move flag: promotion (1), en passant (2), castling (3) -/// NOTE: EN-PASSANT bit is set only when a pawn can be captured +/// NOTE: en passant bit is set only when a pawn can be captured /// /// Special cases are MOVE_NONE and MOVE_NULL. We can sneak these in because in /// any normal move destination square is always different from origin square @@ -124,7 +129,7 @@ enum Move : int { enum MoveType { NORMAL, PROMOTION = 1 << 14, - ENPASSANT = 2 << 14, + EN_PASSANT = 2 << 14, CASTLING = 3 << 14 }; @@ -168,6 +173,11 @@ enum Bound { BOUND_EXACT = BOUND_UPPER | BOUND_LOWER }; +enum ExplosionState { + EXPLOSION_NONE, + MUST_CALM_DOWN +}; + enum Value : int { VALUE_ZERO = 0, VALUE_DRAW = 0, @@ -176,10 +186,12 @@ enum Value : int { VALUE_INFINITE = 32001, VALUE_NONE = 32002, - VALUE_MATE_IN_MAX_PLY = VALUE_MATE - 2 * MAX_PLY, - VALUE_MATED_IN_MAX_PLY = -VALUE_MATE + 2 * MAX_PLY, + VALUE_TB_WIN_IN_MAX_PLY = VALUE_MATE - 2 * MAX_PLY, + VALUE_TB_LOSS_IN_MAX_PLY = -VALUE_TB_WIN_IN_MAX_PLY, + VALUE_MATE_IN_MAX_PLY = VALUE_MATE - MAX_PLY, + VALUE_MATED_IN_MAX_PLY = -VALUE_MATE_IN_MAX_PLY, - PawnValueMg = 128, PawnValueEg = 213, + PawnValueMg = 126, PawnValueEg = 208, KnightValueMg = 781, KnightValueEg = 854, BishopValueMg = 825, BishopValueEg = 915, RookValueMg = 1276, RookValueEg = 1380, @@ -196,23 +208,28 @@ enum PieceType { enum Piece { NO_PIECE, - W_PAWN = 1, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING, - B_PAWN = 9, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING, + W_PAWN = PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING, + B_PAWN = PAWN + 8, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING, PIECE_NB = 16 }; -extern Value PieceValue[PHASE_NB][PIECE_NB]; +constexpr Value PieceValue[PHASE_NB][PIECE_NB] = { + { VALUE_ZERO, PawnValueMg, KnightValueMg, BishopValueMg, RookValueMg, QueenValueMg, VALUE_ZERO, VALUE_ZERO, + VALUE_ZERO, PawnValueMg, KnightValueMg, BishopValueMg, RookValueMg, QueenValueMg, VALUE_ZERO, VALUE_ZERO }, + { VALUE_ZERO, PawnValueEg, KnightValueEg, BishopValueEg, RookValueEg, QueenValueEg, VALUE_ZERO, VALUE_ZERO, + VALUE_ZERO, PawnValueEg, KnightValueEg, BishopValueEg, RookValueEg, QueenValueEg, VALUE_ZERO, VALUE_ZERO } +}; typedef int Depth; enum : int { - DEPTH_QS_CHECKS = 0, DEPTH_QS_NO_CHECKS = -1, DEPTH_QS_RECAPTURES = -5, DEPTH_NONE = -6, - DEPTH_OFFSET = DEPTH_NONE, + + DEPTH_OFFSET = -7 // value used only for TT entry occupancy check }; enum Square : int { @@ -226,7 +243,8 @@ enum Square : int { SQ_A8, SQ_B8, SQ_C8, SQ_D8, SQ_E8, SQ_F8, SQ_G8, SQ_H8, SQ_NONE, - SQUARE_NB = 64 + SQUARE_ZERO = 0, + SQUARE_NB = 64 }; enum Direction : int { @@ -249,6 +267,21 @@ enum Rank : int { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_NB }; +// Keep track of what a move changes on the board (used by NNUE) +struct DirtyPiece { + + // Number of changed pieces + int dirty_num; + + // Max 3 pieces can change in one move. A promotion with capture moves + // both the pawn and the captured piece to SQ_NONE and the piece promoted + // to from SQ_NONE to the capture square. + Piece piece[3]; + + // From and to squares, which may be SQ_NONE + Square from[3]; + Square to[3]; +}; /// Score enum stores a middlegame and an endgame value in a single integer (enum). /// The least significant 16 bits are used to store the middlegame value and the @@ -274,11 +307,11 @@ inline Value mg_value(Score s) { } #define ENABLE_BASE_OPERATORS_ON(T) \ -constexpr T operator+(T d1, T d2) { return T(int(d1) + int(d2)); } \ -constexpr T operator-(T d1, T d2) { return T(int(d1) - int(d2)); } \ +constexpr T operator+(T d1, int d2) { return T(int(d1) + d2); } \ +constexpr T operator-(T d1, int d2) { return T(int(d1) - d2); } \ constexpr T operator-(T d) { return T(-int(d)); } \ -inline T& operator+=(T& d1, T d2) { return d1 = d1 + d2; } \ -inline T& operator-=(T& d1, T d2) { return d1 = d1 - d2; } +inline T& operator+=(T& d1, int d2) { return d1 = d1 + d2; } \ +inline T& operator-=(T& d1, int d2) { return d1 = d1 - d2; } #define ENABLE_INCR_OPERATORS_ON(T) \ inline T& operator++(T& d) { return d = T(int(d) + 1); } \ @@ -296,8 +329,8 @@ inline T& operator/=(T& d, int i) { return d = T(int(d) / i); } ENABLE_FULL_OPERATORS_ON(Value) ENABLE_FULL_OPERATORS_ON(Direction) -ENABLE_INCR_OPERATORS_ON(PieceType) ENABLE_INCR_OPERATORS_ON(Piece) +ENABLE_INCR_OPERATORS_ON(PieceType) ENABLE_INCR_OPERATORS_ON(Square) ENABLE_INCR_OPERATORS_ON(File) ENABLE_INCR_OPERATORS_ON(Rank) @@ -308,12 +341,6 @@ ENABLE_BASE_OPERATORS_ON(Score) #undef ENABLE_INCR_OPERATORS_ON #undef ENABLE_BASE_OPERATORS_ON -/// Additional operators to add integers to a Value -constexpr Value operator+(Value v, int i) { return Value(int(v) + i); } -constexpr Value operator-(Value v, int i) { return Value(int(v) - i); } -inline Value& operator+=(Value& v, int i) { return v = v + i; } -inline Value& operator-=(Value& v, int i) { return v = v - i; } - /// Additional operators to add a Direction to a Square constexpr Square operator+(Square s, Direction d) { return Square(int(s) + int(d)); } constexpr Square operator-(Square s, Direction d) { return Square(int(s) - int(d)); } @@ -343,23 +370,23 @@ inline Score operator*(Score s, int i) { /// Multiplication of a Score by a boolean inline Score operator*(Score s, bool b) { - return Score(int(s) * int(b)); + return b ? s : SCORE_ZERO; } constexpr Color operator~(Color c) { return Color(c ^ BLACK); // Toggle color } -constexpr Square operator~(Square s) { - return Square(s ^ SQ_A8); // Vertical flip SQ_A1 -> SQ_A8 +constexpr Square flip_rank(Square s) { // Swap A1 <-> A8 + return Square(s ^ SQ_A8); } -constexpr Piece operator~(Piece pc) { - return Piece(pc ^ 8); // Swap color of piece B_KNIGHT -> W_KNIGHT +constexpr Square flip_file(Square s) { // Swap A1 <-> H1 + return Square(s ^ SQ_H1); } -inline File map_to_queenside(File f) { - return std::min(f, File(FILE_H - f)); // Map files ABCDEFGH to files ABCDDCBA +constexpr Piece operator~(Piece pc) { + return Piece(pc ^ 8); // Swap color of piece B_KNIGHT <-> W_KNIGHT } constexpr CastlingRights operator&(Color c, CastlingRights cr) { @@ -443,10 +470,6 @@ constexpr Move make_move(Square from, Square to) { return Move((from << 6) + to); } -constexpr Move reverse_move(Move m) { - return make_move(to_sq(m), from_sq(m)); -} - template constexpr Move make(Square from, Square to, PieceType pt = KNIGHT) { return Move(T + ((pt - KNIGHT) << 12) + (from << 6) + to); @@ -456,4 +479,13 @@ constexpr bool is_ok(Move m) { return from_sq(m) != to_sq(m); // Catch MOVE_NULL and MOVE_NONE } +/// Based on a congruential pseudo random number generator +constexpr Key make_key(uint64_t seed) { + return seed * 6364136223846793005ULL + 1442695040888963407ULL; +} + +} // namespace Stockfish + #endif // #ifndef TYPES_H_INCLUDED + +#include "tune.h" // Global visibility to tuning setup diff --git a/src/uci.cpp b/src/uci.cpp index 8b35e6fdd56..741241b3271 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,6 +17,7 @@ */ #include +#include #include #include #include @@ -35,6 +34,8 @@ using namespace std; +namespace Stockfish { + extern vector setup_bench(const Position&, istream&); namespace { @@ -77,6 +78,20 @@ namespace { } } + // trace_eval() prints the evaluation for the current position, consistent with the UCI + // options set so far. + + void trace_eval(Position& pos) { + + StateListPtr states(new std::deque(1)); + Position p; + p.set(pos.fen(), Options["UCI_Chess960"], &states->back(), Threads.main()); + + Eval::NNUE::verify(); + + sync_cout << "\n" << Eval::trace(p) << sync_endl; + } + // setoption() is called when engine receives the "setoption" UCI command. The // function updates the UCI option ("name") to the given value ("value"). @@ -115,7 +130,7 @@ namespace { limits.startTime = now(); // As early as possible! while (is >> token) - if (token == "searchmoves") + if (token == "searchmoves") // Needs to be the last command on the line while (is >> token) limits.searchmoves.push_back(UCI::to_move(pos, token)); @@ -157,7 +172,7 @@ namespace { if (token == "go" || token == "eval") { - cerr << "\nPosition: " << cnt++ << '/' << num << endl; + cerr << "\nPosition: " << cnt++ << '/' << num << " (" << pos.fen() << ")" << endl; if (token == "go") { go(pos, is, states); @@ -165,7 +180,7 @@ namespace { nodes += Threads.nodes_searched(); } else - sync_cout << "\n" << Eval::trace(pos) << sync_endl; + trace_eval(pos); } else if (token == "setoption") setoption(is); else if (token == "position") position(pos, is, states); @@ -182,6 +197,28 @@ namespace { << "\nNodes/second : " << 1000 * nodes / elapsed << endl; } + // The win rate model returns the probability (per mille) of winning given an eval + // and a game-ply. The model fits rather accurately the LTC fishtest statistics. + int win_rate_model(Value v, int ply) { + + // The model captures only up to 240 plies, so limit input (and rescale) + double m = std::min(240, ply) / 64.0; + + // Coefficients of a 3rd order polynomial fit based on fishtest data + // for two parameters needed to transform eval to the argument of a + // logistic function. + double as[] = {-3.68389304, 30.07065921, -60.52878723, 149.53378557}; + double bs[] = {-2.0181857, 15.85685038, -29.83452023, 47.59078827}; + double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; + double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; + + // Transform eval to centipawns with limited range + double x = std::clamp(double(100 * v) / PawnValueEg, -2000.0, 2000.0); + + // Return win rate in per mille (rounded to nearest) + return int(0.5 + 1000 / (1 + std::exp((a - x) / b))); + } + } // namespace @@ -238,9 +275,17 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "flip") pos.flip(); else if (token == "bench") bench(pos, is, states); else if (token == "d") sync_cout << pos << sync_endl; - else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl; + else if (token == "eval") trace_eval(pos); else if (token == "compiler") sync_cout << compiler_info() << sync_endl; - else + else if (token == "export_net") + { + std::optional filename; + std::string f; + if (is >> skipws >> f) + filename = f; + Eval::NNUE::save_eval(filename); + } + else if (!token.empty() && token[0] != '#') sync_cout << "Unknown command: " << cmd << sync_endl; } while (token != "quit" && argc == 1); // Command line args are one-shot @@ -260,7 +305,7 @@ string UCI::value(Value v) { stringstream ss; - if (abs(v) < VALUE_MATE - MAX_PLY) + if (abs(v) < VALUE_MATE_IN_MAX_PLY) ss << "cp " << v * 100 / PawnValueEg; else ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2; @@ -269,6 +314,22 @@ string UCI::value(Value v) { } +/// UCI::wdl() report WDL statistics given an evaluation and a game ply, based on +/// data gathered for fishtest LTC games. + +string UCI::wdl(Value v, int ply) { + + stringstream ss; + + int wdl_w = win_rate_model( v, ply); + int wdl_l = win_rate_model(-v, ply); + int wdl_d = 1000 - wdl_w - wdl_l; + ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; + + return ss.str(); +} + + /// UCI::square() converts a Square to a string in algebraic notation (g1, a7, etc.) std::string UCI::square(Square s) { @@ -318,3 +379,5 @@ Move UCI::to_move(const Position& pos, string& str) { return MOVE_NONE; } + +} // namespace Stockfish diff --git a/src/uci.h b/src/uci.h index b845889bcc1..5bb24a4ec65 100644 --- a/src/uci.h +++ b/src/uci.h @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,6 +24,8 @@ #include "types.h" +namespace Stockfish { + class Position; namespace UCI { @@ -73,10 +73,13 @@ std::string value(Value v); std::string square(Square s); std::string move(Move m, bool chess960); std::string pv(const Position& pos, Depth depth, Value alpha, Value beta); +std::string wdl(Value v, int ply); Move to_move(const Position& pos, std::string& str); } // namespace UCI extern UCI::OptionsMap Options; +} // namespace Stockfish + #endif // #ifndef UCI_H_INCLUDED diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 26fcf30227a..922fa34fe93 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2022 The Stockfish developers (see AUTHORS file) Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,6 +21,7 @@ #include #include +#include "evaluate.h" #include "misc.h" #include "search.h" #include "thread.h" @@ -32,17 +31,20 @@ using std::string; +namespace Stockfish { + UCI::OptionsMap Options; // Global object namespace UCI { /// 'On change' actions, triggered by an option's value change void on_clear_hash(const Option&) { Search::clear(); } -void on_hash_size(const Option& o) { TT.resize(o); } +void on_hash_size(const Option& o) { TT.resize(size_t(o)); } void on_logger(const Option& o) { start_logger(o); } -void on_threads(const Option& o) { Threads.set(o); } +void on_threads(const Option& o) { Threads.set(size_t(o)); } void on_tb_path(const Option& o) { Tablebases::init(o); } - +void on_use_NNUE(const Option& ) { Eval::NNUE::init(); } +void on_eval_file(const Option& ) { Eval::NNUE::init(); } /// Our case insensitive less() function as required by UCI protocol bool CaseInsensitiveLess::operator() (const string& s1, const string& s2) const { @@ -52,34 +54,33 @@ bool CaseInsensitiveLess::operator() (const string& s1, const string& s2) const } -/// init() initializes the UCI options to their hard-coded default values +/// UCI::init() initializes the UCI options to their hard-coded default values void init(OptionsMap& o) { - // at most 2^32 clusters. - constexpr int MaxHashMB = Is64Bit ? 131072 : 2048; + constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; o["Debug Log File"] << Option("", on_logger); - o["Contempt"] << Option(24, -100, 100); - o["Analysis Contempt"] << Option("Both var Off var White var Black var Both", "Both"); o["Threads"] << Option(1, 1, 512, on_threads); o["Hash"] << Option(16, 1, MaxHashMB, on_hash_size); o["Clear Hash"] << Option(on_clear_hash); o["Ponder"] << Option(false); o["MultiPV"] << Option(1, 1, 500); o["Skill Level"] << Option(20, 0, 20); - o["Move Overhead"] << Option(30, 0, 5000); - o["Minimum Thinking Time"] << Option(20, 0, 5000); - o["Slow Mover"] << Option(84, 10, 1000); + o["Move Overhead"] << Option(10, 0, 5000); + o["Slow Mover"] << Option(100, 10, 1000); o["nodestime"] << Option(0, 0, 10000); o["UCI_Chess960"] << Option(false); o["UCI_AnalyseMode"] << Option(false); o["UCI_LimitStrength"] << Option(false); o["UCI_Elo"] << Option(1350, 1350, 2850); + o["UCI_ShowWDL"] << Option(false); o["SyzygyPath"] << Option("", on_tb_path); o["SyzygyProbeDepth"] << Option(1, 1, 100); o["Syzygy50MoveRule"] << Option(true); o["SyzygyProbeLimit"] << Option(7, 0, 7); + o["Use NNUE"] << Option(true, on_use_NNUE); + o["EvalFile"] << Option(EvalFileDefaultName, on_eval_file); } @@ -163,7 +164,7 @@ Option& Option::operator=(const string& v) { assert(!type.empty()); - if ( (type != "button" && v.empty()) + if ( (type != "button" && type != "string" && v.empty()) || (type == "check" && v != "true" && v != "false") || (type == "spin" && (stof(v) < min || stof(v) > max))) return *this; @@ -189,3 +190,5 @@ Option& Option::operator=(const string& v) { } } // namespace UCI + +} // namespace Stockfish diff --git a/tests/instrumented.sh b/tests/instrumented.sh index ae6d5c4b905..e9455eabddc 100755 --- a/tests/instrumented.sh +++ b/tests/instrumented.sh @@ -13,14 +13,14 @@ case $1 in --valgrind) echo "valgrind testing started" prefix='' - exeprefix='valgrind --error-exitcode=42' + exeprefix='valgrind --error-exitcode=42 --errors-for-leak-kinds=all --leak-check=full' postfix='1>/dev/null' threads="1" ;; --valgrind-thread) echo "valgrind-thread testing started" prefix='' - exeprefix='valgrind --error-exitcode=42' + exeprefix='valgrind --fair-sched=try --error-exitcode=42' postfix='1>/dev/null' threads="2" ;; @@ -39,16 +39,16 @@ case $1 in threads="2" cat << EOF > tsan.supp -race:TTEntry::move -race:TTEntry::depth -race:TTEntry::bound -race:TTEntry::save -race:TTEntry::value -race:TTEntry::eval -race:TTEntry::is_pv +race:Stockfish::TTEntry::move +race:Stockfish::TTEntry::depth +race:Stockfish::TTEntry::bound +race:Stockfish::TTEntry::save +race:Stockfish::TTEntry::value +race:Stockfish::TTEntry::eval +race:Stockfish::TTEntry::is_pv -race:TranspositionTable::probe -race:TranspositionTable::hashfull +race:Stockfish::TranspositionTable::probe +race:Stockfish::TranspositionTable::hashfull EOF @@ -70,7 +70,7 @@ for args in "eval" \ "go depth 10" \ "go movetime 1000" \ "go wtime 8000 btime 8000 winc 500 binc 500" \ - "bench 128 $threads 10 default depth" + "bench 128 $threads 8 default depth" do echo "$prefix $exeprefix ./stockfish $args $postfix" @@ -80,7 +80,7 @@ done # more general testing, following an uci protocol exchange cat << EOF > game.exp - set timeout 10 + set timeout 240 spawn $exeprefix ./stockfish send "uci\n" @@ -98,7 +98,7 @@ cat << EOF > game.exp expect "bestmove" send "position fen 5rk1/1K4p1/8/8/3B4/8/8/8 b - - 0 1\n" - send "go depth 30\n" + send "go depth 10\n" expect "bestmove" send "quit\n" @@ -121,7 +121,7 @@ cat << EOF > syzygy.exp send "uci\n" send "setoption name SyzygyPath value ../tests/syzygy/\n" expect "info string Found 35 tablebases" {} timeout {exit 1} - send "bench 128 1 10 default depth\n" + send "bench 128 1 8 default depth\n" send "quit\n" expect eof diff --git a/tests/reprosearch.sh b/tests/reprosearch.sh index 9fd847ff3a9..e16ba4aed91 100755 --- a/tests/reprosearch.sh +++ b/tests/reprosearch.sh @@ -10,7 +10,7 @@ trap 'error ${LINENO}' ERR echo "reprosearch testing started" -# repeat two short games, separated by ucinewgame. +# repeat two short games, separated by ucinewgame. # with go nodes $nodes they should result in exactly # the same node count for each iteration. cat << EOF > repeat.exp @@ -43,7 +43,7 @@ cat << EOF > repeat.exp expect eof EOF -# to increase the likelyhood of finding a non-reproducible case, +# to increase the likelihood of finding a non-reproducible case, # the allowed number of nodes are varied systematically for i in `seq 1 20` do