Add ability to disable performance-sensitive relaxed atomic operations at compile time

Adds a `RelaxedAtomic`  wrapper around either `T` or `std::atomic<T>` and `USE_SLOPPY_ATOMICS` preprocessor define. The intent of this flag is to allow easy disabling of atomics on WASM, where even relaxed atomics are expensive because all atomics have `seq_cst` semantics.

Passed non regression STC
LLR: 2.99 (-2.94,2.94) <-1.75,0.25>
Total: 50624 W: 12976 L: 12776 D: 24872
Ptnml(0-2): 112, 5445, 14005, 5631, 119
https://tests.stockfishchess.org/tests/view/6a1f690e818cacc1db0ad2c7

Passed non-regression STC SMP
 LLR: 2.94 (-2.94,2.94) <-1.75,0.25>
Total: 163696 W: 41514 L: 41438 D: 80744
Ptnml(0-2): 162, 18272, 44904, 18348, 162
https://tests.stockfishchess.org/tests/view/6a21fb97351b79f679cc44b3

Using this class for the TT also allows us to remove the TSAN suppressions, since the UB is fixed.

closes https://github.com/official-stockfish/Stockfish/pull/6877

No functional change
This commit is contained in:
anematode
2026-06-09 21:52:29 +02:00
committed by Joost VandeVondele
parent 415ff793a0
commit 0111d11e23
9 changed files with 115 additions and 61 deletions
+1 -1
View File
@@ -878,7 +878,7 @@ ifeq ($(lsx),yes)
endif
ifeq ($(arch),wasm32)
CXXFLAGS += -pthread -msimd128 -DUSE_POPCNT -DNNUE_EMBEDDING_OFF
CXXFLAGS += -pthread -msimd128 -DUSE_POPCNT -DNNUE_EMBEDDING_OFF -DUSE_SLOPPY_ATOMICS
LDFLAGS += -pthread -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=3MB
ifeq ($(relaxedsimd),yes)
CXXFLAGS += -mrelaxed-simd
+3 -19
View File
@@ -57,28 +57,12 @@ struct StatsEntry {
static_assert(std::is_arithmetic_v<T>, "Not an arithmetic type");
private:
static constexpr bool Atomic =
#ifdef __wasm__
false; // don't use atomics on WASM as they are always seq_cst
#else
Shared;
#endif
std::conditional_t<Atomic, std::atomic<T>, T> entry;
std::conditional_t<Shared, RelaxedAtomic<T>, T> entry;
public:
void operator=(const T& v) {
if constexpr (Atomic)
entry.store(v, std::memory_order_relaxed);
else
entry = v;
}
void operator=(const T& v) { entry = v; }
operator T() const {
if constexpr (Atomic)
return entry.load(std::memory_order_relaxed);
else
return entry;
}
operator T() const { return entry; }
void operator<<(int bonus) {
// Make sure that bonus is in range [-D, D]
+97
View File
@@ -21,6 +21,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstddef>
@@ -339,6 +340,102 @@ class MultiArray {
constexpr void swap(MultiArray<T, Size, Sizes...>& other) noexcept { data_.swap(other.data_); }
};
// Wrapper around std::atomic<T> which uses relaxed accesses or plain
// accesses, depending on the config. Intended use is e.g. wasm where
// the overhead of atomic instructions can be significant, and we only
// require non-tearing for the updates, while ensuring we use relaxed
// accesses otherwise.
template<typename T>
class RelaxedAtomic {
static constexpr bool UseAtomic =
#ifdef USE_SLOPPY_ATOMICS
!std::atomic<T>::is_always_lock_free || sizeof(T) > sizeof(size_t);
#else
true;
#endif
public:
RelaxedAtomic() = default;
RelaxedAtomic(T val) :
inner(val) {}
RelaxedAtomic(const RelaxedAtomic& a) :
inner(static_cast<T>(a)) {}
T operator=(T val) {
if constexpr (UseAtomic)
inner.store(val, std::memory_order_relaxed);
else
inner = val;
return val;
}
RelaxedAtomic& operator=(const RelaxedAtomic& a) {
this->store(static_cast<T>(a), std::memory_order_relaxed);
return *this;
}
operator T() const {
if constexpr (UseAtomic)
return inner.load(std::memory_order_relaxed);
else
return inner;
}
RelaxedAtomic& operator+=(int val) {
T res = this->load(std::memory_order_relaxed) + val;
this->store(res, std::memory_order_relaxed);
return *this;
}
RelaxedAtomic& operator++() {
T res = this->load(std::memory_order_relaxed) + 1;
this->store(res, std::memory_order_relaxed);
return *this;
}
RelaxedAtomic& operator--() {
T res = this->load(std::memory_order_relaxed) - 1;
this->store(res, std::memory_order_relaxed);
return *this;
}
T operator++(int) {
T val = this->load(std::memory_order_relaxed);
this->store(val + 1, std::memory_order_relaxed);
return val;
}
T operator--(int) {
T val = this->load(std::memory_order_relaxed);
this->store(val - 1, std::memory_order_relaxed);
return val;
}
RelaxedAtomic& operator-=(int val) {
T res = this->load(std::memory_order_relaxed) - val;
this->store(res, std::memory_order_relaxed);
return *this;
}
T load(std::memory_order order) const {
assert(order == std::memory_order_relaxed);
if constexpr (UseAtomic)
return inner.load(order);
else
return inner;
}
void store(T val, std::memory_order order) {
assert(order == std::memory_order_relaxed);
if constexpr (UseAtomic)
inner.store(val, order);
else
inner = val;
}
private:
std::conditional_t<UseAtomic, std::atomic<T>, T> inner;
};
// xorshift64star Pseudo-Random Number Generator
// This class is based on original code written and dedicated
+2 -4
View File
@@ -623,8 +623,7 @@ void Search::Worker::do_move(Position& pos, const Move move, StateInfo& st, Stac
void Search::Worker::do_move(
Position& pos, const Move move, StateInfo& st, const bool givesCheck, Stack* const ss) {
bool capture = pos.capture_stage(move);
// Preferable over fetch_add to avoid locking instructions
nodes.store(nodes.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed);
++nodes;
auto [dirtyPiece, dirtyThreats] = accumulatorStack.push();
pos.do_move(move, st, givesCheck, dirtyPiece, dirtyThreats, &tt, &sharedHistory);
@@ -901,8 +900,7 @@ Value Search::Worker::search(
if (err != TB::ProbeState::FAIL)
{
// Preferable over fetch_add to avoid locking instructions
tbHits.store(tbHits.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed);
++tbHits;
int drawScore = tbConfig.useRule50 ? 1 : 0;
+3 -3
View File
@@ -375,9 +375,9 @@ class Worker {
LimitsType limits;
usize pvIdx, pvLast;
std::atomic<u64> nodes, tbHits, bestMoveChanges;
int selDepth, nmpMinPly;
size_t pvIdx, pvLast;
RelaxedAtomic<u64> nodes, tbHits, bestMoveChanges;
int selDepth, nmpMinPly;
Value optimism[COLOR_NB];
+2 -2
View File
@@ -26,8 +26,8 @@
#include <mutex>
#include <vector>
#include "misc.h"
#include "memory.h"
#include "misc.h"
#include "numa.h"
#include "position.h"
#include "search.h"
@@ -168,7 +168,7 @@ class ThreadPool {
std::vector<std::unique_ptr<Thread>> threads;
std::vector<NumaIndex> boundThreadToNumaNode;
u64 accumulate(std::atomic<u64> Search::Worker::* member) const {
u64 accumulate(RelaxedAtomic<u64> Search::Worker::* member) const {
u64 sum = 0;
for (auto&& th : threads)
+7 -7
View File
@@ -79,12 +79,12 @@ struct TTEntry {
friend class TranspositionTable;
friend struct TTWriter;
u16 key16;
u8 depth8;
u8 genBound8;
Move move16;
i16 value16;
i16 eval16;
RelaxedAtomic<u16> key16;
RelaxedAtomic<u8> depth8;
RelaxedAtomic<u8> genBound8;
RelaxedAtomic<Move> move16;
RelaxedAtomic<i16> value16;
RelaxedAtomic<i16> eval16;
};
// Populates the TTEntry with a new node's data, possibly
@@ -214,7 +214,7 @@ void TranspositionTable::clear(ThreadPool& threads) {
const usize start = stride * i;
const usize len = i + 1 != threadCount ? stride : clusterCount - start;
std::memset(&table[start], 0, len * sizeof(Cluster));
std::memset(static_cast<void*>(&table[start]), 0, len * sizeof(Cluster));
});
}
-3
View File
@@ -8,7 +8,6 @@ import fnmatch
from testing import (
EPD,
TSAN,
Stockfish as Engine,
MiniTestFramework,
OrderedClassMembers,
@@ -591,7 +590,6 @@ if __name__ == "__main__":
args = parse_args()
EPD.create_bench_epd()
TSAN.set_tsan_option()
Syzygy.download_syzygy()
framework = MiniTestFramework()
@@ -600,7 +598,6 @@ if __name__ == "__main__":
framework.run([TestCLI, TestInteractive, TestSyzygy, TestEnPassantSanitization])
EPD.delete_bench_epd()
TSAN.unset_tsan_option()
if framework.has_failed():
sys.exit(1)
-22
View File
@@ -43,28 +43,6 @@ class Valgrind:
return ["valgrind", "--error-exitcode=42", "--fair-sched=try"]
class TSAN:
@staticmethod
def set_tsan_option():
with open(f"tsan.supp", "w") as f:
f.write(
"""
race:Stockfish::TTEntry::read
race:Stockfish::TTEntry::save
race:Stockfish::TTWriter::penalize
race:Stockfish::TranspositionTable::probe
race:Stockfish::TranspositionTable::hashfull
"""
)
os.environ["TSAN_OPTIONS"] = "suppressions=./tsan.supp"
@staticmethod
def unset_tsan_option():
os.environ.pop("TSAN_OPTIONS", None)
os.remove(f"tsan.supp")
class EPD:
@staticmethod
def create_bench_epd():