diff --git a/CHANGELOG.md b/CHANGELOG.md index f59449e..4775c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ### Change Log +#### 1.6.0 + +* Optimise ranking flushes. + #### 1.5.0 * Static interface. diff --git a/CMakeLists.txt b/CMakeLists.txt index a915c26..7fb4d48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(${PROJECT_NAME}) # Versioning. set(SK_POKER_EVAL_VERSION_MAJOR 1) -set(SK_POKER_EVAL_VERSION_MINOR 5) +set(SK_POKER_EVAL_VERSION_MINOR 6) set(SK_POKER_EVAL_VERSION_PATCH 0) # Get the current commit. diff --git a/README.md b/README.md index e805eee..c358699 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ A lightweight 32-bit Texas Hold'em 7-card hand evaluator written in C++. [![Build Status](https://travis-ci.org/kennethshackleton/SKPokerEval.svg)](https://travis-ci.org/kennethshackleton/SKPokerEval) -## Example +## How do I use it? -``` +```cpp #include #include "SevenEval.h" @@ -18,3 +18,25 @@ int main() { return 0; } ``` + +## Why does it work? + +We exploit a key-scheme that gives us just enough uniqueness to correctly identify the integral rank of any 7-card hand, where the greater this rank is the better the hand we hold and two hands of the same rank always draw. Typically we require six additions and a memory footprint just shy of 400kB. + +To start with we computed by brute force the first thirteen non-negative integers such that the sum of exactly seven with each taken at most four times is unique among all such sums: 0, 1, 5, 22, 98, 453, 2031, 8698, 22854, 83661, 262349, 636345 and 1479181. A valid sum might be 0+0+1+1+1+1+5 = 9 or 0+98+98+453+98+98+1 = 846, but invalid sum expressions include 0+262349+0+0+0+1 (too few summands), 1+1+5+22+98+453+2031+8698 (too many summands), 0+1+5+22+98+453+2031+8698 (again too many summands, although 1+5+22+98+453+2031+8698 is a legitimate expression) and 1+1+1+1+1+98+98 (too many 1's). We assign these integers as the card face values and add these together to generate a key for any non-flush 7-card hand. The largest non-flush key we see is 7825759, corresponding to any of the four quad-of-aces-full-of-kings. + +Similarly, we assign the integer values 0, 1, 8 and 57 for spade, heart, diamond and club respectively. Any sum of exactly seven values taken from {0, 1, 8, 57} is unique among all such sums. We add up the suits of a 7-card hand to produce a "flush check" key and use this to find a pre-calculated flush suit value (in the case we're looking at a flush) or otherwise a defined non-flush constant. The largest flush key we see is 7999, corresponding to any of the four 7-card straight flushes with ace high. + +The extraordinarily lucky aspect of this is that the maximum non-flush key we have, 7825759, is a 23-bit integer (note 2^23 = 8388608) and the largest suit key we find, 57*7 = 399, is a 9-bit integer (note 2^9 = 512). If we bit-shift a card's non-flush face value and add to this its flush check to make a card key in advance, when we aggregate the resulting card keys over a given 7-card hand we generate a 23+9 = 32-bit integer key for the whole hand. This integer key can only just be accommodated on a 32-bit machine and yet still carries enough information to decide if we're looking at a flush and if not to then look up the rank of the hand. + +## How might I profile my contribution? + +The project contains a [profiler](src/Profiler.cpp) which might be used to help benchmark your changes. + +```bash +g++ -c -std=c++11 -O3 Profiler.cpp +g++ -o profile Profiler.o +./profile +``` + +Crudely, the lower the result the more efficiently the ranks were computed. This starts to be compelling with consistent gains of, say, 30% or more. diff --git a/src/Constants.h b/src/Constants.h index 6764210..d82743f 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -30,6 +30,11 @@ #define DIAMOND 8 #define CLUB 57 +#define INDEX_SPADE 0 +#define INDEX_HEART 1 +#define INDEX_DIAMOND 2 +#define INDEX_CLUB 3 + #define TWO_FIVE 0 #define THREE_FIVE 1 #define FOUR_FIVE 5 diff --git a/src/Deckcards.h b/src/Deckcards.h index 80b9944..889e21a 100644 --- a/src/Deckcards.h +++ b/src/Deckcards.h @@ -23,7 +23,7 @@ #include "Constants.h" #include -uint32_t const card[DECK_SIZE] = { +uint_fast32_t const card[DECK_SIZE] = { (ACE << NON_FLUSH_BIT_SHIFT) + SPADE, (ACE << NON_FLUSH_BIT_SHIFT) + HEART, (ACE << NON_FLUSH_BIT_SHIFT) + DIAMOND, @@ -90,23 +90,23 @@ uint32_t const card[DECK_SIZE] = { (TWO << NON_FLUSH_BIT_SHIFT) + CLUB }; -uint16_t const suit[DECK_SIZE] = { - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB, - SPADE, HEART, DIAMOND, CLUB +uint_fast8_t const suit[DECK_SIZE] = { + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB, + INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB }; -uint16_t const flush[DECK_SIZE] = { +uint_fast16_t const flush[DECK_SIZE] = { ACE_FLUSH, ACE_FLUSH, ACE_FLUSH, ACE_FLUSH, KING_FLUSH, KING_FLUSH, KING_FLUSH, KING_FLUSH, QUEEN_FLUSH, QUEEN_FLUSH, QUEEN_FLUSH, QUEEN_FLUSH, @@ -122,4 +122,67 @@ uint16_t const flush[DECK_SIZE] = { TWO_FLUSH, TWO_FLUSH, TWO_FLUSH, TWO_FLUSH }; +uint_fast16_t const flushes[NUMBER_OF_SUITS][DECK_SIZE] = { + { + ACE_FLUSH, 0, 0, 0, + KING_FLUSH, 0, 0, 0, + QUEEN_FLUSH, 0, 0, 0, + JACK_FLUSH, 0, 0, 0, + TEN_FLUSH, 0, 0, 0, + NINE_FLUSH, 0, 0, 0, + EIGHT_FLUSH, 0, 0, 0, + SEVEN_FLUSH, 0, 0, 0, + SIX_FLUSH, 0, 0, 0, + FIVE_FLUSH, 0, 0, 0, + FOUR_FLUSH, 0, 0, 0, + THREE_FLUSH, 0, 0, 0, + TWO_FLUSH, 0, 0, 0 + }, + { + 0, ACE_FLUSH, 0, 0, + 0, KING_FLUSH, 0, 0, + 0, QUEEN_FLUSH, 0, 0, + 0, JACK_FLUSH, 0, 0, + 0, TEN_FLUSH, 0, 0, + 0, NINE_FLUSH, 0, 0, + 0, EIGHT_FLUSH, 0, 0, + 0, SEVEN_FLUSH, 0, 0, + 0, SIX_FLUSH, 0, 0, + 0, FIVE_FLUSH, 0, 0, + 0, FOUR_FLUSH, 0, 0, + 0, THREE_FLUSH, 0, 0, + 0, TWO_FLUSH, 0, 0 + }, + { + 0, 0, ACE_FLUSH, 0, + 0, 0, KING_FLUSH, 0, + 0, 0, QUEEN_FLUSH, 0, + 0, 0, JACK_FLUSH, 0, + 0, 0, TEN_FLUSH, 0, + 0, 0, NINE_FLUSH, 0, + 0, 0, EIGHT_FLUSH, 0, + 0, 0, SEVEN_FLUSH, 0, + 0, 0, SIX_FLUSH, 0, + 0, 0, FIVE_FLUSH, 0, + 0, 0, FOUR_FLUSH, 0, + 0, 0, THREE_FLUSH, 0, + 0, 0, TWO_FLUSH, 0 + }, + { + 0, 0, 0, ACE_FLUSH, + 0, 0, 0, KING_FLUSH, + 0, 0, 0, QUEEN_FLUSH, + 0, 0, 0, JACK_FLUSH, + 0, 0, 0, TEN_FLUSH, + 0, 0, 0, NINE_FLUSH, + 0, 0, 0, EIGHT_FLUSH, + 0, 0, 0, SEVEN_FLUSH, + 0, 0, 0, SIX_FLUSH, + 0, 0, 0, FIVE_FLUSH, + 0, 0, 0, FOUR_FLUSH, + 0, 0, 0, THREE_FLUSH, + 0, 0, 0, TWO_FLUSH + } +}; + #endif diff --git a/src/FlushCheck.h b/src/FlushCheck.h index 7e7e84c..aca0ed6 100644 --- a/src/FlushCheck.h +++ b/src/FlushCheck.h @@ -22,27 +22,27 @@ #include -int8_t const flush_check[400] = { - 0, 0, 0, -2, -2, 1, 1, 1, 0, 0, -2, -2, -2, 1, 1, -1, 0, -2, -2, -2, -2, 1, - -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, 8, 8, - 8, -1, -1, -1, -1, -1, 8, 8, -1, -1, -1, -1, -1, -1, 8, 0, 0, -2, -2, -2, 1, 1, - -1, 0, -2, -2, -2, -2, 1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, - -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, 8, 8, -1, -1, -1, -1, -1, - -1, 8, -1, -1, -1, -1, -1, -1, -1, -1, 0, -2, -2, -2, -2, 1, -1, -1, -2, -2, - -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, - -1, -1, -2, -2, -1, -1, -1, -1, -1, -1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, - -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -1, -1, -2, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, - -2, -2, -1, -1, -1, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, 57, 57, 57, -1, -1, -1, -1, -1, 57, 57, -1, -1, -1, -1, -1, -1, 57, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 57, 57, - -1, -1, -1, -1, -1, -1, 57, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 57 +int_fast8_t const flush_check[400] = { + 0, 0, 0, -2, -2, 1, 1, 1, 0, 0, -2, -2, -2, 1, 1, -1, 0, -2, -2, -2, -2, 1, + -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, 2, 2, + 2, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 2, 0, 0, -2, -2, -2, 1, 1, + -1, 0, -2, -2, -2, -2, 1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, + -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, + -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, 0, -2, -2, -2, -2, 1, -1, -1, -2, -2, + -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, + -1, -1, -2, -2, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, -1, -2, -2, -2, -2, -1, + -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -1, -1, -2, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -2, -2, -2, -2, -1, -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, -1, + -2, -2, -1, -1, -1, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 3, 3, 3, -1, -1, -1, -1, -1, 3, 3, -1, -1, -1, -1, -1, -1, 3, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, + -1, -1, -1, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3 }; #endif // SKPOKEREVAL_FLUSHCHECK_H diff --git a/src/Profiler.cpp b/src/Profiler.cpp index 79c561b..7d48464 100644 --- a/src/Profiler.cpp +++ b/src/Profiler.cpp @@ -37,7 +37,7 @@ class Profiler { static clock_t Profile(unsigned const count) { std::default_random_engine gen; std::uniform_int_distribution dist(0, 51); - int const length = 7*count; + int const length = 28*count; unsigned * const buffer = (unsigned *) malloc(length * sizeof(unsigned)); for (int i = 0; i < length; i += 7) { int j = 0; @@ -54,11 +54,23 @@ class Profiler { } } std::clock_t const start = std::clock(); - for (int i = 0; i < length; i += 7) { + for (int i = 0; i < length; i += 28) { + doNotOptimiseAway( + SevenEval::GetRank(buffer[i+21], buffer[i+22], buffer[i+23], + buffer[i+24], buffer[i+25], buffer[i+26], buffer[i+27]) + ); doNotOptimiseAway( SevenEval::GetRank(buffer[i], buffer[i+1], buffer[i+2], buffer[i+3], buffer[i+4], buffer[i+5], buffer[i+6]) ); + doNotOptimiseAway( + SevenEval::GetRank(buffer[i+7], buffer[i+8], buffer[i+9], + buffer[i+10], buffer[i+11], buffer[i+12], buffer[i+13]) + ); + doNotOptimiseAway( + SevenEval::GetRank(buffer[i+14], buffer[i+15], buffer[i+16], + buffer[i+17], buffer[i+18], buffer[i+19], buffer[i+20]) + ); } std::clock_t const end = std::clock(); delete buffer; @@ -66,12 +78,19 @@ class Profiler { } }; +float clocksToMilliseconds(clock_t c) { + return 1000.0f * c / CLOCKS_PER_SEC; +} + int main() { clock_t fastest = std::numeric_limits::max(); for (int i = 0; i < 20; ++i) { - fastest = std::min(fastest, Profiler::Profile(50000000)); + clock_t const profile = Profiler::Profile(12500000); + fastest = std::min(fastest, profile); + std::cout << i << ": " << clocksToMilliseconds(profile) << "ms" + << std::endl; } - std::cout << 1000.0f * fastest / CLOCKS_PER_SEC << " ms" << std::endl; + std::cout << "Result: " << clocksToMilliseconds(fastest) << "ms" << std::endl; } #endif diff --git a/src/SevenEval.h b/src/SevenEval.h index c0ab4c4..e867f43 100644 --- a/src/SevenEval.h +++ b/src/SevenEval.h @@ -39,23 +39,16 @@ class SevenEval { uint_fast32_t key = card[i] + card[j] + card[k] + card[l] + card[m] + card[n] + card[p]; // Tear off the flush check strip. - int_fast8_t const flush_suit = flush_check[key & SUIT_BIT_MASK]; - if (NOT_A_FLUSH == flush_suit) { + int_fast8_t const suit = flush_check[key & SUIT_BIT_MASK]; + if (NOT_A_FLUSH == suit) { // Tear off the non-flush key strip, and look up the rank. key >>= NON_FLUSH_BIT_SHIFT; return rank_hash[offsets[key >> RANK_OFFSET_SHIFT] + (key & RANK_HASH_MOD)]; } // Generate a flush key, and look up the rank. - int flush_key = 0; - if (suit[i] == flush_suit) flush_key = flush[i]; - if (suit[j] == flush_suit) flush_key += flush[j]; - if (suit[k] == flush_suit) flush_key += flush[k]; - if (suit[l] == flush_suit) flush_key += flush[l]; - if (suit[m] == flush_suit) flush_key += flush[m]; - if (suit[n] == flush_suit) flush_key += flush[n]; - if (suit[p] == flush_suit) flush_key += flush[p]; - return flush_ranks[flush_key]; + uint_fast16_t const * const s = flushes[suit]; + return flush_ranks[s[i] + s[j] + s[k] + s[l] + s[m] + s[n] + s[p]]; } };