Custom Tooling
Custom Tooling¶
- High-performance Polyhedral Dice
- Function Timer
File: Dice.hpp
¶
C++ header-only library
#pragma once
#include <random>
#include <algorithm>
namespace Dice {
static std::mt19937_64 Engine { std::random_device()() };
long long random_int(long long left, long long right) {
std::uniform_int_distribution<long long> distribution {
std::min(left, right), std::max(right, left)
};
return distribution(Engine);
}
long long d(long long sides) {
std::uniform_int_distribution<long long> distribution {1, sides};
return distribution(Engine);
}
long long dice(long long rolls, long long sides) {
long long total {0};
for (long long i {0}; i < rolls; ++i) {
total += d(sides);
}
return total;
}
} // end namespace
File: Dice.pyx
¶
Cython file
#!python3
#distutils: language = c++
import time
import math
import statistics
__all__ = ("shuffle", "dice", "d", "random_int", "func_timer")
cdef extern from "Dice.hpp":
long long _random_int "Dice::random_int"(long long, long long)
long long _dice "Dice::dice"(long long, long long)
long long _d "Dice::d"(long long)
def random_int(left: int, right: int):
""" Random Integer
Inclusive, flat uniform distribution, where input order does not matter.
@param left :: typically the lower of the two inputs.
@param right :: typically the higher of the two inputs.
@return :: Returns a random number in range [left, right].
"""
return _random_int(left, right)
def d(sides: int):
""" Random Die, d(10) is d10
Simulates a random roll of a fair polyhedral die.
@param sides :: The size of the die (number of sides).
@return :: Returns the value of a random die roll.
"""
return _d(sides)
def dice(rolls: int, sides: int):
""" Random Dice, dice(3, 6) is 3d6
Simulates many random rolls of a fair polyhedral die.
@param rolls :: The number of dice rolled.
@param sides :: The size of dice rolled (number of sides).
@return :: Returns the sum total of the dice rolled.
"""
return _dice(rolls, sides)
def shuffle(array: list):
""" Knuth B Shuffle Algorithm
Destructive, in-place shuffle.
@param array :: list of values.
@return :: None
"""
size = len(array) - 1
for i in reversed(range(size)):
j = _random_int(i, size)
array[i], array[j] = array[j], array[i]
def func_timer(func: staticmethod, *args, **kwargs):
""" Function Timer
@param func :: The function to be analyzed.
@param *args :: Optional arguments to be passed to `func`
@param **kwargs :: Optional arguments to be passed to `func`
@return :: None
"""
results = []
outer_cycles = 64
inner_cycles = 16
for i in range(outer_cycles):
start = time.time()
for _ in range(inner_cycles):
_ = func(*args, **kwargs)
total = time.time() - start
results.append(total / inner_cycles)
err = statistics.stdev(results) * 1e+9 / 2
t_time = min(results) * 1e+9 + err
print(f"Timing: {int(math.ceil(t_time))} ± {int(math.ceil(err))} ns")
File: setup.py
¶
from setuptools import setup, Extension
from Cython.Build import cythonize
with open("README.md", "r") as file:
long_description = file.read()
setup(
name="Dice",
ext_modules=cythonize(
Extension(
name="Dice",
sources=["Dice.pyx"],
language=["c++"],
extra_compile_args=["-std=gnu++11"],
),
compiler_directives={
'embedsignature': True,
'language_level': 3,
},
),
author="Robert Sharp",
author_email="webmaster@sharpdesigndigital.com",
requires=["Cython"],
version="0.0.1",
description="Fast Dice",
long_description=long_description,
long_description_content_type="text/markdown",
classifiers=["Development Status :: 1 - Planning"],
keywords=["dice", "polyhedral", "random integer"],
python_requires='>=3.6',
)
File: READMEME.md
¶
Dice Module Documentation
# Dice
High-performance dice functions for Python3.
`Dice.d(sides: int) -> int`
- Represents a single roll of a given size die.
- @param sides :: Represents the size or number of sides, most commonly six.
- @return :: Returns a random integer in the range [1, sides].
- Flat uniform distribution.
`Dice.dice(rolls: int, sides: int) -> int`
- Represents the sum total of multiple rolls of the same size die.
- @param rolls :: Represents the number of times to roll the die.
- @param sides :: Represents the die size or number of sides, most commonly six.
- @return :: Returns a random integer in range [X, Y] where X = rolls and Y = rolls * sides.
- Geometric distribution based on the number and size of the dice rolled.
- Complexity scales primarily with the number of rolls, not the size of the dice.
`Dice.random_int(left_limit: int, right_limit: int) -> int`
- @param left_limit :: Any Integer
- @param right_limit :: Any Integer
- @return :: Returns a random integer in the range [left_limit, right_limit]
- `random_int(1, 10) -> [1, 10]`
- `random_int(10, 1) -> [1, 10]` same as above.
- `random_int(A, B)` Always returns A when A == B
- Flat uniform distribution.
`Dice.shuffle(array: list) -> None`
- Knuth B shuffle algorithm. Destructive, in-place shuffle.
- Far more cache-friendly than the builtin Random.shuffle()
- @param array :: List to be shuffled.
- @return :: None
`Dice.func_timer(func: staticmethod, *args, **kwargs) -> None`
- Function Timer
- @param func :: The function to be analyzed.
- @param *args :: Optional arguments to be passed to `func`
- @param **kwargs :: Optional arguments to be passed to `func`
- @return :: None
File: dice_tests.py
¶
Performance Test Script
This test script will only work once we install the Dice module.
import Dice
import random
def py_d(num_sides: int) -> int:
return random.randint(1, num_sides)
def py_dice(num_rolls: int, num_sides: int) -> int:
return sum(py_d(num_sides) for _ in range(num_rolls))
print("\n# Dice Performance Tests #")
print('-' * 26)
print("\nTest 1: Random Integer 1-100")
lo, hi = 1, 100
print(f"Random.randint({lo}, {hi}):", end=' ')
Dice.func_timer(random.randint, lo, hi)
print(f"Dice.random_int({lo}, {hi}):", end=' ')
Dice.func_timer(Dice.random_int, lo, hi)
print("\nTest 2: Single Dice Roll d100")
sides = 100
print(f"py_d({sides}):", end=' ')
Dice.func_timer(py_d, sides)
print(f"Dice.d({sides}):", end=' ')
Dice.func_timer(Dice.d, sides)
print("\nTest 3: Multi Dice Roll 10d10")
rolls, sides = 10, 10
print(f"py_dice({rolls}, {sides}):", end=' ')
Dice.func_timer(py_dice, rolls, sides)
print(f"Dice.dice({rolls}, {sides}):", end=' ')
Dice.func_timer(Dice.dice, rolls, sides)
Typical Test Output
# Dice Performance Tests #
--------------------------
Test 1: Random Integer 1-100
Random.randint(1, 100): Timing: 1412 ± 41 ns
Dice.random_int(1, 100): Timing: 69 ± 24 ns
Test 2: Single Dice Roll d100
py_d(100): Timing: 1507 ± 31 ns
Dice.d(100): Timing: 61 ± 16 ns
Test 3: Multi Dice Roll 10d10
py_dice(10, 10): Timing: 16868 ± 745 ns
Dice.dice(10, 10): Timing: 377 ± 20 ns
Installation and Deployment¶
Install
$ pip install .
Test
$ python3
>>> import Dice
>>> Dice.d(10) # d10
7
>>> Dice.dice(8, 6) # 8d6
34
Deploy
$ python3 setup.py sdist bdist_wheel
$ twine upload dist/*
Network Install
$ pip uninstall Dice
$ pip install Dice
Test Again
$ python3
>>> import Dice
>>> Dice.d(10) # d10
8
>>> Dice.dice(8, 6) # 8d6
25
Run the performance tests
$ python3 dice_tests.py
# Dice Performance Tests #
--------------------------
Test 1: Random Integer 1-100
Random.randint(1, 100): Timing: 1412 ± 294 ns
Dice.random_int(1, 100): Timing: 62 ± 17 ns
Test 2: Single Dice Roll d100
py_d(100): Timing: 1768 ± 561 ns
Dice.d(100): Timing: 56 ± 12 ns
Test 3: Multi Dice Roll 10d10
py_dice(10, 10): Timing: 14918 ± 851 ns
Dice.dice(10, 10): Timing: 316 ± 18 ns
Process finished with exit code 0