Skip to content

Commit

Permalink
Merge branch 'release/v1.7'
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethshackleton committed Aug 19, 2017
2 parents 24c71be + 6b72550 commit bc2a3e6
Show file tree
Hide file tree
Showing 17 changed files with 7,339 additions and 9,354 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,5 +1,6 @@
.DS_Store
.idea
.vscode

# CMake
CMakeCache.txt
Expand Down
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -17,4 +17,3 @@ install:
fi

script: ./build.sh

5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,10 @@
### Change Log

#### 1.7.0

* Reduce potential memory footprint by 40%.
* Slight performance gains.

#### 1.6.0

* Optimise ranking flushes.
Expand Down
4 changes: 3 additions & 1 deletion CMakeLists.txt
Expand Up @@ -5,7 +5,7 @@ project(${PROJECT_NAME})

# Versioning.
set(SK_POKER_EVAL_VERSION_MAJOR 1)
set(SK_POKER_EVAL_VERSION_MINOR 6)
set(SK_POKER_EVAL_VERSION_MINOR 7)
set(SK_POKER_EVAL_VERSION_PATCH 0)

# Get the current commit.
Expand Down Expand Up @@ -41,5 +41,7 @@ add_subdirectory(lib/gtest-1.7.0)
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})

add_executable(eval_tests tests/five_eval_tests.cpp tests/seven_eval_tests.cpp)

target_link_libraries(eval_tests gtest_main skpokereval)
add_test(NAME GTests COMMAND eval_tests)

12 changes: 6 additions & 6 deletions README.md
Expand Up @@ -19,17 +19,17 @@ int main() {
}
```

## Why does it work?
## How 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.
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. We require a memory footprint of 250kB and typically six additions to rank a hand.

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.
To start with we computed by brute force the first thirteen non-negative integers such that the formal 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.
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, and the largest suit key is 399.

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?
## I want to contribute, how might I profile my change?

The project contains a [profiler](src/Profiler.cpp) which might be used to help benchmark your changes.

Expand All @@ -39,4 +39,4 @@ 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.
For optimisations this starts to be compelling with consistent gains of, say, 30% or more.
1 change: 0 additions & 1 deletion build.sh
@@ -1,3 +1,2 @@
#!/bin/sh
./configure && cmake . && make && ctest --verbose

21 changes: 7 additions & 14 deletions scripts/perfect_hash.py
Expand Up @@ -49,18 +49,13 @@
print "Key count is %i." % (len(keys),)
print "Max key is %i." % (max_key,)

side = 1 # Power of 2 to ultimately optimise hash key calculation.
exp = 0
while side*side <= max_key:
side <<= 1
exp += 1

side = 128 # Power of 2 to ultimately optimise hash key calculation.
print "Square will be of side %i." % (side,)

square = [[-1]*side for i in xrange(side)]
square = [[-1]*(512*side) for i in xrange(side)]

for k in keys:
square[k & (side - 1)][k >> exp] = k
square[k % side][k / side] = k

offset = [0]*((max_key / side) + 1)
hash_table = [-1]*max_key
Expand All @@ -84,12 +79,10 @@
(i, j, hash_table_len)
break

f = open('./hash_table', 'w')
f.write("%s\n" % (hash_table[0:hash_table_len],))
f.close()
with open('./hash_table_%s' % (side,), 'w') as f:
f.write("%s\n" % (hash_table[0:hash_table_len],))

f = open('./offset', 'w')
f.write("%s\n" % (offset,))
f.close()
with open('./offset_%s' % (side,), 'w') as f:
f.write("%s\n" % (offset,))

print "Hash table has length %i." % (hash_table_len,)
10 changes: 5 additions & 5 deletions src/Constants.h
Expand Up @@ -91,17 +91,17 @@
#define MAX_SEVEN_FLUSH_KEY_INT (ACE_FLUSH+KING_FLUSH+QUEEN_FLUSH+JACK_FLUSH+\
TEN_FLUSH+NINE_FLUSH+EIGHT_FLUSH)

#define RANK_OFFSET_SHIFT 12
#define RANK_HASH_MOD 4095
#define RANK_OFFSET_SHIFT 7
#define RANK_HASH_MOD 127

#define MAX_FLUSH_CHECK_SUM (7*CLUB)

// Used in flush checking. These must be distinct from each of the suits.
#define UNVERIFIED -1
#define NOT_A_FLUSH -2
#define UNVERIFIED (-1)
#define NOT_A_FLUSH (-2)

// Bit masks
#define NON_FLUSH_BIT_SHIFT 9
#define SUIT_BIT_MASK (1<<NON_FLUSH_BIT_SHIFT)-1
#define SUIT_BIT_MASK ((1<<NON_FLUSH_BIT_SHIFT)-1)

#endif // SKPOKEREVAL_CONSTANTS_H_
18 changes: 1 addition & 17 deletions src/Deckcards.h
Expand Up @@ -106,23 +106,7 @@ uint_fast8_t const suit[DECK_SIZE] = {
INDEX_SPADE, INDEX_HEART, INDEX_DIAMOND, INDEX_CLUB
};

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,
JACK_FLUSH, JACK_FLUSH, JACK_FLUSH, JACK_FLUSH,
TEN_FLUSH, TEN_FLUSH, TEN_FLUSH, TEN_FLUSH,
NINE_FLUSH, NINE_FLUSH, NINE_FLUSH, NINE_FLUSH,
EIGHT_FLUSH, EIGHT_FLUSH, EIGHT_FLUSH, EIGHT_FLUSH,
SEVEN_FLUSH, SEVEN_FLUSH, SEVEN_FLUSH, SEVEN_FLUSH,
SIX_FLUSH, SIX_FLUSH, SIX_FLUSH, SIX_FLUSH,
FIVE_FLUSH, FIVE_FLUSH, FIVE_FLUSH, FIVE_FLUSH,
FOUR_FLUSH, FOUR_FLUSH, FOUR_FLUSH, FOUR_FLUSH,
THREE_FLUSH, THREE_FLUSH, THREE_FLUSH, THREE_FLUSH,
TWO_FLUSH, TWO_FLUSH, TWO_FLUSH, TWO_FLUSH
};

uint_fast16_t const flushes[NUMBER_OF_SUITS][DECK_SIZE] = {
uint_fast16_t const suit_kronecker[NUMBER_OF_SUITS][DECK_SIZE] = {
{
ACE_FLUSH, 0, 0, 0,
KING_FLUSH, 0, 0, 0,
Expand Down
1 change: 0 additions & 1 deletion src/FiveEval.cpp
Expand Up @@ -186,4 +186,3 @@ uint16_t FiveEval::GetRank(int const card_one, int const card_two,
}
return best_rank_so_far;
}

31 changes: 17 additions & 14 deletions src/Profiler.cpp
Expand Up @@ -24,7 +24,7 @@
#include <algorithm>
#include <random>
#include <iostream>
#include <ctime>
#include <chrono>
#include <limits>

template <class T>
Expand All @@ -34,7 +34,7 @@ inline void doNotOptimiseAway(T&& datum) {

class Profiler {
public:
static clock_t Profile(unsigned const count) {
static double RandomAccessProfile(unsigned const count) {
std::default_random_engine gen;
std::uniform_int_distribution<int> dist(0, 51);
int const length = 28*count;
Expand All @@ -53,7 +53,7 @@ class Profiler {
if (accept) buffer[i+(j++)] = r;
}
}
std::clock_t const start = std::clock();
auto const start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < length; i += 28) {
doNotOptimiseAway(
SevenEval::GetRank(buffer[i+21], buffer[i+22], buffer[i+23],
Expand All @@ -72,25 +72,28 @@ class Profiler {
buffer[i+17], buffer[i+18], buffer[i+19], buffer[i+20])
);
}
std::clock_t const end = std::clock();
auto const end = std::chrono::high_resolution_clock::now();
delete buffer;
return end-start;
return 1e-6 * std::chrono::duration_cast<std::chrono::nanoseconds>(
end - start).count();
}
};

float clocksToMilliseconds(clock_t c) {
return 1000.0f * c / CLOCKS_PER_SEC;
}

int main() {
clock_t fastest = std::numeric_limits<clock_t>::max();
std::cout << "Profiling SevenEval random access..." << std::endl;
long const numberOfHands = 50000000L;
auto fastest = std::numeric_limits<double>::max();
for (int i = 0; i < 20; ++i) {
clock_t const profile = Profiler::Profile(12500000);
auto const profile = Profiler::RandomAccessProfile(numberOfHands / 4);
fastest = std::min(fastest, profile);
std::cout << i << ": " << clocksToMilliseconds(profile) << "ms"
<< std::endl;
std::cout << i << ": " << profile << " ms" << std::endl;
}
std::cout << "Best random access time: " << fastest << " ms" << std::endl;
if (fastest > 0.0) {
double const handsPerSecond = (numberOfHands * 1000L) / fastest;
std::cout << "Best random access rate: " << handsPerSecond
<< " hands/sec" << std::endl;
}
std::cout << "Result: " << clocksToMilliseconds(fastest) << "ms" << std::endl;
}

#endif

0 comments on commit bc2a3e6

Please sign in to comment.