Single-threaded perft on Zen 4 and Zen 5

Discussion of chess software programming and technical issues.

Moderator: Ras

vtlmks
Posts: 2
Joined: Fri Aug 07, 2026 4:59 am
Full name: Peter Fors

Single-threaded perft on Zen 4 and Zen 5

Post by vtlmks »

Hello,

I have published Mindchess, a C99 legal-move perft engine with separately tuned AMD Zen 4 and Zen 5 kernels:

https://github.com/vtlmks/mindchess

It is single-threaded, uses no hashing or transposition table, and visits every position. The same move generator is used for every FEN; it does not recognize benchmark positions or select algorithms from position signatures.

The Zen 5 comparison below was measured on the 32 MiB L3 CCD of a Ryzen 9 9950X3D with stock boost enabled. CPU 9 and its SMT sibling 25 were isolated from normal processes, IRQs and unbound workqueues. The timed process ran under taskset and SCHED_FIFO priority 99.

For comparison, I used Chessbit by Thomas Huijbregts as the reference move generator. The Chessbit numbers come from the AVX2 Windows executable distributed with his project, run under Wine 11.14. Wine's persistent service threads were pinned to another core, leaving only the timed Chessbit process on CPU 9.

Mindchess values are medians of five runs and Chessbit values are medians of three runs. Rates are million nodes per second.

Code: Select all

Position    Depth          Nodes  Mindchess  Chessbit    Difference
Start           7  3,195,901,860    3582.48   2774.52       +29.12%
Kiwipete        6  8,031,647,685    4536.99   4002.58       +13.35%
Midgame         6  6,923,051,137    5172.02   4583.86       +12.83%
Endgame         7 24,958,831,314    5079.09   4289.61       +18.40%
Across the 43,109,431,996 final-depth nodes, Mindchess produced 4835.64 MNPS and Chessbit produced 4110.65 MNPS, a node-weighted lead of 17.64%. Every reported node count matched. I did not use Chessbit’s displayed aggregate because its timer includes the preceding depths while its aggregate node count includes only the four final depths.

The tested 9950X3D exhibited a false dependency on the destination register of distinct-register TZCNT instructions. This motivated a separate Zen 5 kernel. Repairing those instructions after register allocation, together with different scheduling and lookup organization, recovered a substantial regression seen when running the retained Zen 4 kernel on this CPU.

The repository contains the complete Zen 4 and Zen 5 results, correctness tests, build scripts, a native source comparison with pinned Chessbit source, performance-counter measurements, and notes about the retained and rejected optimizations.

These are local measurements for the stated processor and software configuration, not a claim about performance on other CPUs.

The README contains the complete build and measurement procedure, correctness checks, performance-counter data, and notes on retained and rejected variants. I have tried to document the likely technical questions there.
dangi12012
Posts: 1071
Joined: Tue Apr 28, 2020 10:03 pm
Full name: Daniel Infuehr

Re: Single-threaded perft on Zen 4 and Zen 5

Post by dangi12012 »

I love the momentum in the movegen community.

I can add one gold nugget of things you can try of my things I never could (due to my lack of true AVX512). The hyperbola quiesce approach maps very very very well onto the galois field instruction. https://github.com/Gigantua/Chess_Moveg ... erbola.hpp
_mm512_gf2p8affine_epi64_epi8 or even better its _mm256 variant. This could be much faster than pext.

Furthermore you could have a board structure that maintains the forwards and backwards occ during movegen (all moves also do pos ^ 56) which then makes bit_bswap F R E E.

Code: Select all

	/* Generate attack using the hyperbola quintessence approach */
	static constexpr uint64_t attack(uint64_t pieces, uint32_t x, uint64_t mask) {
		uint64_t o = pieces & mask;
		return ((o - (1ull << x)) ^ bit_bswap(bit_bswap(o) - (0x8000000000000000ull >> x))) & mask; //Daniel 28.04.2022 - Faster shift. Replaces (1ull << (s ^ 56))
	}
Worlds-fastest-Bitboard-Chess-Movegenerator
Daniel Inführ - Software Developer
vtlmks
Posts: 2
Joined: Fri Aug 07, 2026 4:59 am
Full name: Peter Fors

Re: Single-threaded perft on Zen 4 and Zen 5

Post by vtlmks »

Thanks for the suggestion. I tested GFNI Hyperbola Quintessence separately against the PEXT lookup currently used by mindchess.

This was on a Ryzen 9 9950X3D, boost disabled, isolated core, GCC 16.1.1, `-O3 -flto=auto -march=native -mtune=native`.

The GFNI version packs the two independent slider lines into the two 64-bit lanes of an XMM register. Bit reversal uses `GF2P8AFFINE` for reversing bits within each byte followed by `PSHUFB` for reversing the bytes within each 64-bit lane.

Correctness was verified for all 64 source squares, bishops and rooks, every relevant PEXT blocker subset, plus random/sparse/dense/empty/full occupancies. All implementations produced identical results.

TSC cycles per lookup:

Code: Select all

                  PEXT     Scalar HQ    GFNI HQ

Bishop serial      9.462      27.589      29.374
Rook serial       20.973      27.597      29.403
Mixed serial      15.217      27.593      29.389

Bishop independent 1.726      21.492       4.336
Rook independent   3.305      21.493       4.341
Mixed independent  2.516      21.493       4.338
The independent result is the relevant one for mindchess. The depth-2 generator already exposes multiple independent PEXT + table-load chains, so Zen 5 overlaps most of the latency. That takes the mixed PEXT case from 15.2 cycles serial to 2.52 cycles/lookup with eight independent chains.

GFNI HQ also benefits heavily from independence, but stops at about 4.34 cycles/lookup. PEXT therefore has about 72% higher throughput in the representative mixed case.

The generated code also explains the difference. The independent PEXT loop is about 7.4 instructions/lookup. GFNI HQ remains around 27 instructions/lookup because it needs the two line calculations, affine transforms, byte shuffles, subtractions, masks and lane reduction.

I did not test maintaining a permanently reversed occupancy. That would eliminate the first GFNI+PSHUFB reversal, but the reverse subtraction result still has to be reversed, and maintaining another occupancy representation adds work to make/unmake. With the lookup already at 4.34 versus 2.52 cycles, I don't see enough remaining headroom to justify changing the board representation.

So the GFNI mapping works and was interesting to test, but it doesn't fit the way mindchess currently generates sliders on Zen 5. The important difference is that the bad serial latency of PEXT + table lookup is largely hidden by the independent chains already present in the generator.