Custom Tooling

19_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

screen
$ pip install .

Test

screen
$ python3
>>> import Dice
>>> Dice.d(10)  # d10
7
>>> Dice.dice(8, 6)  # 8d6
34

Deploy

screen
$ python3 setup.py sdist bdist_wheel
$ twine upload dist/*

Network Install

$ pip uninstall Dice
$ pip install Dice

Test Again

screen
$ python3
>>> import Dice
>>> Dice.d(10)  # d10
8
>>> Dice.dice(8, 6)  # 8d6
25

Run the performance tests

screen
$ 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