Skip to content

Commit 9e46478

Browse files
thecppzooEduardoEd
authored
Em/pokerbotic subsumption (#68)
LGTM. Only looked at the commit that updates repo to function under swar. merging. * Compile time classification * Refactored combinatorials out of main * Refactoring of SWAR operations * Refactoring of SWAR part 2 * Completes generic implementation of popcount * Renamed file * Changes * Start of classification function * Counting function * Corrects greater equal * Implements straight * Almost complete implementation of winner * Benchmarking * Refactoring * Refactoring 2 * File copy Poker_io.h * Refactoring * Renaming Numbers to Ranks * Benchmarks * Refactoring * Adding inc/ep/Classifications.h from Poker.h * Refactoring * Refactoring * inc/ep/CascadeComparisons.h from Poker.h * Refactoring * Clarification * No comparison function * Egyptian multiplication algorithm for straights, hand ranking * Corrected known bugs * First bug corrected by tests * Tests * Corrects Counted::clearAt and Full House results * Removes unwanted check * More tests * Completes tests, implements progressive subsets, benchmarks hand ranks * Fastest hand ranking among known codebases! * Obsoleted dual representation * Partial implementation of CardSet * Flushes proved * Straights tested * Improved performance: clear of boolean is an XOR * Tests Ok * Corrected hand rank comparison bug * Extends Floyd algorithm for preselected subsets * Create communities.md * Update communities.md * Update communities.md * Classifications, attempt 2 * SuitedPocket tested * All pockets tested * Kazone * Create README.md * Update README.md * Create Floyd-Sampling.md * Update Floyd-Sampling.md * Rename Floyd-Sampling.md to Fastest-Floyd-Sampling.md * Create What-is-noak-or-how-to-determine-pairs.md * Update What-is-noak-or-how-to-determine-pairs.md * Update What-is-noak-or-how-to-determine-pairs.md * Update What-is-noak-or-how-to-determine-pairs.md * Create Hand-Ranking.md * Update README.md * Add files via upload * Removes root/third_party, makes vscode workspace all relative --------- Co-authored-by: Eduardo <emadrid@crabel.com> Co-authored-by: Ed <ed@Eds-MacBook-Pro.local> Co-authored-by: Eddie <eddie see email elsewhere>
1 parent 0dfdd96 commit 9e46478

35 files changed

+2780
-22
lines changed

pokerbotic/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Pokerbotic
2+
3+
## This repository will be changing very soon to incorporate feedback from my CPPCon 2019 presentation, please check back in a couple of weeks
4+
5+
**Pokerbotic** is a poker engine. It has been developed by a professional software engineer and a semi-professional poker player with professional knowledge of stochastic processes little by little.
6+
7+
Currently, we have the hand evaluator framework, that achieves in normally available machines a rate of 100 million evaluations per second, that is, it classifies more than 100 million poker hands into what "four of a kind", etc. they are.
8+
9+
**The code today assumes the AMD64 architecture**, and support of the [BMI2 instructions](https://en.wikipedia.org/wiki/Bit_Manipulation_Instruction_Sets#BMI2_.28Bit_Manipulation_Instruction_Set_2.29). AMD64/Intel is not essential to this code, just that the necessary adaptations have not been made. You are welcome to help with this.
10+
11+
Currently, the code is a header-only framework with some use cases programmed in C++ 14.
12+
13+
This code beats other poker engines, including the popular open source framework "PokerStove" both on ease of use and performance due to the application of Generic Programming.
14+
15+
Generic Programming allows hoisting what otherwise would be run-time computation to compilation time, this is illustrated in the non-trivial `static_assert` in the code itself.
16+
17+
The documentation for the advanced programming techniques, including the Floyd sampling algorithm, the SWAR techniques is being written.
18+
19+
## How to build it
20+
21+
### Prerequisites:
22+
23+
1. GCC compatible compiler. We recommend Clang 3.9 or 4.0 specifically. Benchmarks indicate Clang gives noticeably faster code. The code uses GCC extensions in the way of builtins.
24+
2. C++ 14. In GCC or Clang, do not forget the option `-std=c++14`
25+
3. Support for BMI2 instructions, activated with `-march=native` (preferred way) or specifically with `-mbmi2`
26+
4. Test cases require the ["Catch" testing framework](https://github.com/philsquared/Catch).
27+
5. Currently the code does not require a Unix/POSIX operating system (this code should be compilable in Windows64 through either gcc or clang), however, **we reserve the option to make the code incompatible with any operating system**.
28+
29+
### There are several test programs available:
30+
31+
#### Unit tests at [src/main.cpp](https://github.com/thecppzoo/pokerbotic/blob/master/src/main.cpp)
32+
33+
Several unit tests. This program illustrates how to use the engine framework. To build it, at the project root, you may do this:
34+
35+
`clang++ -std=c++14 -Iinc -DTESTS -O3 -march=native -I../Catch/include src/main.cpp -o main`
36+
37+
Notice you have to define TESTS and indicate the path to the "Catch" testing framework.
38+
39+
#### [src/benchmarks.cpp](https://github.com/thecppzoo/pokerbotic/blob/master/src/benchmarks.cpp)
40+
41+
A program that measures the execution speed of several internal mechanisms. To build it, at the project root, you may do this:
42+
43+
`clang++ -std=c++14 -Iinc -DBENCHMARKS -O3 -march=native src/benchmarks.cpp -o benchmarks`
44+
45+
This program can be run without arguments. It will generate all 7-card hands and time the execution of all evaluations.
46+
47+
#### [src/comparisonBenchmark.cpp](https://github.com/thecppzoo/pokerbotic/blob/master/src/comparisonBenchmark.cpp)
48+
49+
This program generates as in Texas Hold'em Poker, all possible 5-community cards, and proceeds to iterate over all two-player 2-card "pocket cards".
50+
51+
Because of the size of this search space, this program emits a current tally of execution every 100 million cases.
52+
53+
To build, for example:
54+
55+
`clang++ -std=c++14 -Iinc -DHAND_COMPARISON -O3 -march=native -o cb src/comparisonBenchmark.cpp`
56+
57+
Can be run without arguments.
58+
59+
## Next feature to be implemented
60+
61+
Currently, multithreaded partitioning of evaluations is being implemented.
62+
63+
## Documentation/User manual
64+
65+
Not yet written. Most of the code available under the folder `ep/` is fully operational.
66+
7.35 MB
Binary file not shown.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
The Floyd sampling algorithm --you can see an excellent exposition [here](http://www.nowherenearithaca.com/2013/05/robert-floyds-tiny-and-beautiful.html)-- is very convenient for use cases such as getting a hand of cards from a deck.
2+
3+
The fastest way to represent sets of finite and small domains (such as a deck of cards) seems to be as bits in bitfields.
4+
5+
For an example of a deck of 52 cards, we may want, for example, to generate all of the 7-card hands. I wrote an straightforward implementation [here](https://github.com/thecppzoo/pokerbotic/blob/master/inc/ep/Floyd.h). Its interface is this:
6+
7+
```c++
8+
template<int N, int K, typename Rng>
9+
inline uint64_t floydSample(Rng &&g);
10+
```
11+
12+
With speed in mind, the size of the set and subset are template parameters. Compilers such as Clang, GCC routinely generate optimal code for the given sizes, as can be seen in the compiler explorer, which means they take advantage of those parameters being template parameters. The return value is the subset expressed as the bits set in the least significant N bits of the resulting integer.
13+
14+
However, what if the use case is to generate a sample (subset) of the *remaining* members of the set? for example, to generate a random 2-card *after* five cards have been selected?
15+
16+
That has been implemented too, in a function with this signature:
17+
18+
```c++
19+
template<int N, int K, typename Rng>
20+
inline uint64_t floydSample(Rng &&g, uint64_t preselected)
21+
```
22+
23+
Here, `preselected` represents the cards already selected. If what is desired is to get two cards from the cards remaining after selecting `fiveAlreadySelected` cards, the call `ep::floydSample<47, 2>(randomGenerator, fiveAlreadySelected)` will suffice. Notice the template argument for `N` is now 47, reflecting the fact that the remaining set of cards has 47 cards. Unfortunately, it is difficult to guarantee at compilation time that the argument `fiveAlreadySelected` indeed has exactly five elements, because operations such as intersection or union result in sets with cardinalities that are fundamentally run-time values.
24+
25+
This overload for `ep::floydSample` requires calling a "deposit" operation. This is an interesting operation hard to implement without direct support from the processor: Given a mask, the bits of the input will be "deposited" one at a time into the bit positions indicated as bits set in the mask. In the AMD64/Intel architecture EM64T this is supported in the instruction set "BMI2" as the instruction [`PDEP`](https://chessprogramming.wikispaces.com/BMI2). The implementation of the adaptation of the Floyd algorithm for a known number of preselected elements is then straightforward: discount from the total the number of bits set, call normal floydSample, and "deposit" the result in the inverse of the preselection.
26+
27+
What are the costs of these implementations?
28+
29+
1. The programmer needs to indicate at compilation time the number of elements in the set. If this number is a runtime value, a `switch` will be needed to convert runtime to compile time numbers, that transforms into an indexed jump at the assembler level.
30+
2. All of the operations in the normal Floyd sampling algorithm are negligible in terms of execution costs compared to calling the random number generator, which is essential in each iteration.
31+
3. The adaptation to account for preselections only requires two assembler instructions more: inverting the preselection and depositing it. `PDEP` has been measured to be an instruction with a throughput of one per clock, which is excellent compared to implementing it in software; however, in current processors it can only be executed in a particular pipeline. In Pokerbotic we don't think we are oversubscribing this pipeline, so we suspect we get a 1-per-clock throughput for this use case.
32+
4. However, the adaptation to account for preselections also require the programmer to accurately indicate the cardinality of the preselection. This can add the same cost as number 1 here, plus the population count, another single-pipeline, 1-per-clock throughput instruction.
33+
34+
We are interested in any way to implement a faster subset sample selection. This use case is at the heart of many operations in Pokerbotic.
35+

pokerbotic/design/Hand-Ranking.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Design of the hand classification mechanism in Pokerbotic
2+
3+
## Detection of N-of-a-kind
4+
5+
Detection of N-of-a-kind, two pairs and full house is described [here](https://github.com/thecppzoo/pokerbotic/blob/master/design/What-is-noak-or-how-to-determine-pairs.md).
6+
7+
## Detection of flush
8+
9+
Flush detection happens at [Poker.h:78](https://github.com/thecppzoo/pokerbotic/blob/master/inc/ep/Poker.h#L78) the hand is filtered per each suit, and the built in for population count on the filtered set is called. This code assumes a hand can only have one suit in flush (or that the hand has up to 9 cards).
10+
11+
## Detection of straights
12+
13+
The straightforward way to detect straights, if the ranks would be consecutive bits (which we will call "packed representation") is this:
14+
15+
```c++
16+
unsigned straights(unsigned cards) {
17+
auto shifted1 = cards << 1;
18+
auto shifted2 = cards << 2;
19+
auto shifted3 = cards << 3;
20+
auto shifted4 = cards << 4;
21+
return cards & shifted1 & shifted2 & shifted3 & shifted4
22+
}
23+
```
24+
25+
By shifting and doing a conjuction at the end, the only bits in the result set to one are those that are succeeded by four consecutive bits set to one. Before accounting for the aces to work as "ace or one", there are possible improvements to be discussed:
26+
27+
### Checking for the presence of 5 or 10
28+
29+
In a deck of 13 ranks starting with 2, all straights must have either the rank 5 or the rank ten. This has a probability of nearly a third; however, testing for this explicitly is performance disadvantageous. It seems the branch is fundamentaly not predictable by the processor, so, the penalty of misprediction overcompensates the benefit of early exit. In the code above, there are 8 binary operators and 4 compile-time constants, there is little budget for branch misprediction. Older versions of the code had this check until it was benchmarked to be a disadvantage.
30+
31+
### Checking for partial conjunctions
32+
33+
For the same reason, testing if any of the conjunctions is zero to return 0 early is not performance advantageous, confirmed through benchmarking.
34+
35+
### Addition chain
36+
37+
There is one improvement that benchmarks confirm:
38+
39+
```c++
40+
unsigned straights(unsigned cards) {
41+
// assume the point of view from the bit position for tens.
42+
auto shift1 = cards >> 1;
43+
// in shift1 the bit for the rank ten now contains the bit for jacks
44+
auto tj = cards & shift1;
45+
auto shift2 = tj >> 2;
46+
// in shift2, the position for the rank ten now contains the conjunction of originals queen and king
47+
auto tjqk = tj & shift2;
48+
return tjqk & (cards >> 4);
49+
}
50+
```
51+
52+
This implementation (which does not take into account the ace duality) requires 6 binary operations and 3 constants and accomplishes the same thing as the straightforward implementation. Benchmarks confirm this taking roughly 3/4 of the time than the straightforward implementation.
53+
54+
The key insight here is to view the detection of the straight as adding up to 5 starting with 1. The straightforward implementation does the equivalent of `1 + 1 + 1 + 1 + 1`, this new implementation does `auto two = 1 + 1; return two + two + 1`. This technique is to build an *addition chain*. This technique was inspired by the second chapter of the book ["From Mathematics To Generic Programming"](https://www.amazon.com/Mathematics-Generic-Programming-Alexander-Stepanov/dp/0321942043)
55+
56+
Taking into account the dual rank of aces is simply to turn on the 'ones' if there is the ace, but this requires left shift to make room for it. This can be done at the beginning of the straight check, and its cost can be amortized by the compiler doing a conditional move early, meaning the result will be ready by the time it is used.
57+
58+
There is one further complication in the code, which is that the engine uses the rank-array representation. Provided that the shifts are for 4, 8, 12, 16 bits instead of 1, 2, 3, 4 there isn't yet a difference. There are two needs for straights:
59+
60+
1. Normal straights, in which the suit of the rank does not matter. This is accomplished by making the 13 rank counts as described in how to detect pairs, etc., and using the SWAR operation `greaterEqual<1>` prior to the straight code. Naturally, the straights don't incurr in an extra cost of doing popcounts because they are amortized in the necessary part of detection of pairs, three of a kind, etc., the `greaterEqual<N>(arg)` operation requires two constants and two or three assembler operations, depending on how the result is used, thus, for practical purposes have negligible cost compared to a packed rank representation.
61+
2. Straights to detect straight flush: Since the bits for
62+
63+
We suspect our detection of straight code is maximal in terms of performance.

0 commit comments

Comments
 (0)