The problem is easy to state. Given seven cards, produce a single comparable number such that a better poker hand always yields a better number. Do it hundreds of millions of times, because that is what a Monte Carlo equity calculation costs.
It is a good problem to study because it has an unusually clean structure — the answer space is tiny, the input space is small but not trivial, and the entire design space is a trade between precomputation and work at query time.
The numbers that constrain the design
Four counts govern everything:
| Quantity | Value |
|---|---|
| Distinct 5-card hands | 2,598,960 |
| Distinct 7-card hands | 133,784,560 |
| Distinct 5-card hand values | 7,462 |
| 5-card subsets of 7 cards | 21 |
The third row is the one that makes fast evaluation possible. Nearly 2.6 million five-card hands collapse into 7,462 equivalence classes once you account for suit symmetry and for the fact that many hands tie. So the output of an evaluator is a number in a range that fits comfortably in 13 bits, and the entire question is how cheaply you can get from an input to that number.
The fourth row is why seven-card evaluation is not simply five-card evaluation: the best five of seven must be selected, and the obvious way to select it is to try all 21.
Approach 1: enumerate and compare
Generate all 21 five-card subsets, evaluate each with a five-card evaluator, keep the maximum.
Correct, obvious, and the reference implementation everything else is tested against. Its cost is 21 evaluations plus the subset generation, which sounds bad and is bad — but note that it makes any five-card evaluator into a seven-card evaluator for free, which is why it survives as the fallback path in a lot of code.
Approach 2: classify directly
Sort the seven cards, count rank multiplicities, check for five of one suit, check for five consecutive ranks, and branch through the category hierarchy from straight flush downward.
This is what a human does and what a UI wants. It is readable, it needs no tables at all, and it produces the category as a by-product, which matters if you are rendering "Full house, kings over threes" rather than comparing two numbers. It is also branchy, and branchy code on modern hardware pays for mispredictions in a way that a table lookup does not.
Every card game with a rules display has one of these. It is the right choice when you evaluate a few hundred hands a second and the wrong choice when you evaluate ten million.
Approach 3: the prime-product trick
The best-known five-card evaluator, published by an author writing as Cactus Kev, encodes each card as a 32-bit word packing four things: a one-hot bit for the rank, a one-hot bit for the suit, the rank index, and a small prime number assigned to the rank (2 for deuce, 3 for three, 5 for four, and so on).
That packing makes three questions cheap:
- Is it a flush? Bitwise-AND the four suit bits across all five cards. Non-zero means all five share a suit.
- What ranks are present? Bitwise-OR the rank bits across all five cards, giving a 13-bit mask. If exactly five bits are set, no rank repeats, so the hand is a high card, a straight, or the rank pattern of a flush — all of which can be looked up directly by that mask.
- What is the multiset of ranks? Multiply the five primes together. Because prime factorisation is unique, the product identifies the multiset of ranks exactly, regardless of the order the cards arrived in, and regardless of suit.
That last step is the elegant one. A pair of kings and three sevens produces one specific integer that no other rank multiset can produce. So the remaining work is to map that product to a hand value, which the original implementation did with a binary search over a sorted array of the valid products.
The binary search is the weak point — it is a handful of unpredictable branches and scattered memory accesses on every evaluation. Later variants replaced it with a hash.
Approach 4: perfect hashing
A perfect hash maps a known, fixed key set to distinct slots with no collisions, which means no probing and no comparison chain: compute an index, read one array entry, done.
Applied here it takes two forms. The modest one replaces the prime-product binary search with a hash of the product into a table sized a little above the number of distinct products. The ambitious one hashes the card set itself — a 52-bit mask with seven bits set — straight to a hand value, eliminating the five-card decomposition entirely. Published implementations of the second exist and are compact enough to keep in cache.
Finding the magic constants is a one-off search performed offline. That is the characteristic cost of this family: build time is nontrivial and query time is close to a single dependent load.
Approach 5: the big lookup table
The approach usually attributed to a thread on the Two Plus Two forums abandons cleverness for volume. It precomputes a state machine as a flat array of 32-bit integers. Start at a root offset, index by the first card to get a new offset, index that by the second card, and so on for all seven cards. The value you land on after the seventh step encodes both the hand category and the rank within it.
Seven dependent array lookups and no branches at all. No sorting, no suit logic, no subset enumeration, no special-casing of straights. The rules are all baked into the table's structure.
The price is the table. In the commonly circulated form it holds roughly 32 million 32-bit entries, which is on the order of 130 MB, and it takes real time to generate — long enough that implementations serialise it to disk and memory-map it at startup rather than rebuilding it.
That size is the whole argument against it. A 130 MB table does not fit in any cache. Every one of the seven lookups is a potential main-memory access, and the random access pattern defeats prefetching. On a machine doing nothing else it is extremely fast. Inside a simulation that is also touching its own working set, it evicts everything and the measured advantage narrows or reverses.
The actual trade-off
| Approach | Table size | Build cost | Query shape |
|---|---|---|---|
| Enumerate 21 subsets | none, or the 5-card table | none | 21 evaluations |
| Direct classification | none | none | sorting plus many branches |
| Prime product + search | small (tens of KB) | trivial | few ops plus a binary search |
| Perfect hash | small to moderate | offline search | arithmetic plus one or two loads |
| Full 7-card table | ~130 MB | minutes, cached to disk | seven dependent loads, no branches |
Read down the "table size" column and you have the entire history of the field. Every generation traded memory for branches, and then the memory hierarchy changed and some of the trades reversed. The modern preference leans back toward compact tables plus a little arithmetic, because a table that stays resident beats a table that is theoretically faster per lookup and is never in cache.
Which is a general lesson worth more than the specific answer: precomputation is only free when the precomputed thing fits somewhere fast. The same reasoning decides whether a database index earns its keep.
Testing an evaluator
There is a rare luxury here — the input space is small enough to test exhaustively.
- Evaluate all 133,784,560 seven-card hands and compare against the naive 21-subset reference. Any disagreement is a bug, with no sampling and no doubt.
- Histogram the categories across that exhaustive sweep and compare against the combinatorial counts computed independently. This catches whole classes of off-by-one error in category boundaries, particularly around the wheel straight (A-2-3-4-5) and the steel wheel, which is where most evaluators are wrong first.
- Check that equal-value hands compare equal. Splitting a pot incorrectly is a correctness bug that a simple "is A better than B" test suite will not surface.
The wheel is worth calling out specifically, because it is the one place the rank ordering is not monotonic: the ace is simultaneously the highest rank and the bottom of the lowest straight. Prime-product and bitmask implementations both need an explicit special case for it, and it is the single most common source of a subtly wrong evaluator.
Choosing
If you are rendering hands to a screen, write the readable classifier. If you are running equity simulations, use a compact table-based evaluator — a perfect-hash implementation or a modern bitwise one — and measure it inside your actual workload rather than in a tight microbenchmark that reports cache-resident numbers you will never see again.
And measure before you optimise at all. In a great many equity calculations the evaluator is not the bottleneck; the random number generation, the deck shuffling, or the allocation churn around them is.