libnum: unit tests, perf tests for num16, double limits

* Add a unit testing framework in libnum/test/. Assertions are checked
  against sparse sets of input values (a couple thousands for each
  type), distributed fractally.
* Add performance tests for num16.
* Fix an overly ambitious substitution of /256 by >>8 in num16::mul,
  which would give some incorrect results for negative results.
* Also fix an incorrect sign extension in the num16->num32 conversion.
* Express comparison-with-int operators in terms of the integer even
  though some versions are faster when expressed in terms of the fixed-
  point value. This is because the integer is frequently known at
  compile-time.
* Add minDouble and maxDouble static members to each num type to
  programmatically supply the bounds of the type.
This commit is contained in:
Lephenixnoir 2022-07-24 00:05:41 +01:00
parent 967eb034f4
commit 708ba1b017
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
9 changed files with 566 additions and 39 deletions

View File

@ -8,9 +8,9 @@
include(CTest)
add_library(num STATIC
src/static_checks.cpp)
src/str.cpp)
target_include_directories(num PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_include_directories(num PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
# Library file: libnum.a
install(TARGETS num DESTINATION ${LIBDIR})
@ -18,13 +18,29 @@ install(TARGETS num DESTINATION ${LIBDIR})
install(DIRECTORY include/ DESTINATION ${INCDIR})
#---
# Testing
# Unit tests
#---
set(TESTS
test/isel_num8.cpp)
set(UNIT_TESTS
test/unit_scalar.cpp
test/unit_static.cpp)
foreach(testfile IN LISTS TESTS)
if(NOT CMAKE_CROSSCOMPILING)
add_executable(numtest ${UNIT_TESTS})
target_link_libraries(numtest PUBLIC num)
add_test(NAME "UnitTests" COMMAND numtest)
endif()
#---
# Performance tests
#---
set(PERF_TESTS
test/isel_num8.cpp
test/isel_num16.cpp)
foreach(testfile IN LISTS PERF_TESTS)
add_test(NAME "${testfile}"
COMMAND python "${CMAKE_CURRENT_SOURCE_DIR}/test/isel.py"
"${CMAKE_CURRENT_SOURCE_DIR}/${testfile}"

View File

@ -19,10 +19,6 @@
General idea for an fp -> num conversion:
1. Literally just shift mantissa by exponent - num_fixed_position */
/* TODO: Template specializations for std::integral_constant<int, VALUE> that
inlines at compile time to either (1) true/false if out of bounds, or (2)
coerce the int to the fixed point type */
#pragma once
#include <cstdint>
@ -103,15 +99,19 @@ struct num8
inline constexpr bool operator<(int const &i) {
return i >= 1;
}
inline constexpr bool operator>=(int const &i) {
return i <= 0;
}
inline constexpr bool operator<=(int const &i) {
return i + !v > 0;
}
inline constexpr bool operator>(int const &i) {
return i + !v <= 0;
}
inline constexpr bool operator>=(int const &i) {
return i <= 0;
}
/* Limits as double */
static constexpr double minDouble = 0.0;
static constexpr double maxDouble = double(0xff) / 256;
};
/* num16: Signed 8:8 fixed-point type
@ -148,7 +148,7 @@ struct num16
inline constexpr explicit operator double() { return (double)v / 256; }
/* num16 x num16 -> num32 multiplication
This is efficiently implemented with a muls.l instruction. */
This is efficiently implemented with a muls.w instruction. */
static constexpr num32 dmul(num16 const &x, num16 const &y);
/* Basic arithmetic */
@ -162,7 +162,7 @@ struct num16
return *this;
}
inline constexpr num16 &operator*=(num16 const &other) {
v = (v * other.v) >> 8;
v = (v * other.v) / 256;
return *this;
}
inline constexpr num16 &operator/=(num16 const &other) {
@ -177,20 +177,27 @@ struct num16
/* Comparisons with int */
inline constexpr bool operator==(int const &i) {
return ((v & 0xff) == 0) && (v >> 8) == i;
return (int16_t)i == i && (i << 8) == v;
}
inline constexpr bool operator<(int const &i) {
return (v >> 8) < i;
}
inline constexpr bool operator>=(int const &i) {
return (v >> 8) >= i;
}
/* Unfortunately the branchless version for this test is expressed in terms
of `v`, not `i`, so it does not simplify well when `i` is known. In that
case, writing eg. `x > num16(0)` is faster than `x > 0`. */
inline constexpr bool operator<=(int const &i) {
return (v >> 8) + ((v & 0xff) != 0) <= i;
}
inline constexpr bool operator>(int const &i) {
return (v >> 8) + ((v & 0xff) != 0) > i;
}
inline constexpr bool operator>=(int const &i) {
return (v >> 8) >= i;
}
/* Limits as double */
static constexpr double minDouble = -128.0;
static constexpr double maxDouble = double(0x7fff) / 256;
};
/* num32: Signed 16:16 fixed-point type
@ -257,6 +264,28 @@ struct num32
v %= other.v;
return *this;
}
/* Comparisons with int */
inline constexpr bool operator==(int const &i) {
return (int16_t)i == i && (i << 16) == v;
}
inline constexpr bool operator<(int const &i) {
return (v >> 16) < i;
}
inline constexpr bool operator>=(int const &i) {
return (v >> 16) >= i;
}
inline constexpr bool operator<=(int const &i) {
return (v >> 16) + ((v & 0xffff) != 0) <= i;
}
inline constexpr bool operator>(int const &i) {
return (v >> 16) + ((v & 0xffff) != 0) > i;
}
/* Limits as double */
static constexpr double minDouble = -32768.0;
static constexpr double maxDouble = double(0x7fffffff) / 65536;
};
/* Arithmetic with integers */
@ -331,6 +360,11 @@ struct num64
v %= other.v;
return *this;
}
/* Limits as double; note that the double doesn't have enough precision to
represent the entirety of the maximum value. */
static constexpr double minDouble = -2147483648.0;
static constexpr double maxDouble = 2147483648.0 - double(1) / 2147483648;
};
/* The following concept identifies the four num types */
@ -355,7 +389,7 @@ inline constexpr num16::num16(num32 n): v((uint32_t)n.v >> 8) {}
inline constexpr num16::num16(num64 n): v(n.v >> 24) {}
inline constexpr num32::num32(num8 n): v(n.v * 256) {}
inline constexpr num32::num32(num16 n): v(n.v * 256) {}
inline constexpr num32::num32(num16 n): v((int32_t)n.v * 256) {}
inline constexpr num32::num32(num64 n): v(n.v >> 16) {}
inline constexpr num64::num64(num8 n): v((uint64_t)n.v * 16777216) {}
@ -369,7 +403,6 @@ template<typename T> requires(is_num<T>)
inline constexpr bool operator==(T const &left, T const &right) {
return left.v == right.v;
}
template<typename T> requires(is_num<T>)
inline constexpr bool operator!=(T const &left, T const &right) {
return left.v != right.v;

View File

@ -1,8 +1,10 @@
#include <num/num.h>
using namespace num;
/* Digits of the decimal part, from most to least significant. Returns the
number of digits (which is 0 when x=0) */
static int decimal_digits(char *str, num64 x)
{
// x = mod_64(x, num64_const(1));
// x = mod_64(x, num64_const(1));
return 0;
}

View File

@ -35,7 +35,7 @@
# is a shortcut for the number of "non-trivial" instructions, and currently
# expands to `[!mov && !rts]`.
#
# A test is a normal C++ source built using the library, which exposes
# A test is a normal C++ source file built using the library, which exposes
# functions with C linkage (ie. no name mangling) and has specifications in
# comments of the form:
#

View File

@ -0,0 +1,78 @@
#include <num/num.h>
using namespace num;
extern "C" {
// num16_of_num8: %=1 && [extu.b]
num16 num16_of_num8(num8 x)
{
return num16(x);
}
// num16_of_num32: %=1 && [shlr8]
num16 num16_of_num32(num32 x)
{
return num16(x);
}
// num16_of_num64: %=[sh* || or]
num16 num16_of_num64(num64 x)
{
return num16(x);
}
// num16_mul: [shad] && ![jsr]
num16 num16_mul(num16 x, num16 y)
{
return x * y;
}
// num16_dmul: [muls.w]
num32 num16_dmul(num16 x, num16 y)
{
return num16::dmul(x, y);
}
// num16_eq: [bt* || bf*] && [shll8]
bool num16_eq(num16 x, int i)
{
return x == i;
}
// num16_le: ![bt* || bf*]
bool num16_le(num16 x, int i)
{
return x <= i;
}
// num16_gt: ![bt* || bf*]
bool num16_gt(num16 x, int i)
{
return x > i;
}
// num16_le_0: %<=3
bool num16_le_0(num16 x)
{
return x <= num16(0);
}
// num16_ge_0: %<=3 || (%=4 && [mov.l])
bool num16_ge_0(num16 x)
{
return x >= num16(0);
}
// num16_gt_0: %<=3
bool num16_gt_0(num16 x)
{
return x > num16(0);
}
// num16_lt_0: %<=3 || (%=4 && [mov.l])
bool num16_lt_0(num16 x)
{
return x < num16(0);
}
} /* extern "C" */

102
libnum/test/unit_check.h Normal file
View File

@ -0,0 +1,102 @@
//---------------------------------------------------------------------------//
// ," /\ ", Azur: A game engine for CASIO fx-CG and PC //
// | _/__\_ | Designed by Lephe' and the Planète Casio community. //
// "._`\/'_." License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// unit_check.h: Utilities for asserting and printing
//
// This header defines a ToString<T> typeclass and the utility functions
// runWithChecker() which extend the provided function with a Checker argument.
// This object accumulates the results of tests, and stores the names and
// values of the test subjects so they can be printed if any assertion fails.
//---
#include <string>
#include <array>
#include <tuple>
#include <stdio.h>
#include <num/num.h>
template<typename T>
struct ToString {};
template<typename T>
concept to_string = requires { ToString<T>::str; };
template<typename T> requires(is_num<T>)
struct ToString<T>
{
static std::string str(T x) {
char s[64];
uint64_t v = (uint64_t)x.v;
if constexpr (sizeof x < 8)
v &= ((1ull << 8 * sizeof x) - 1);
sprintf(s, "%0*lx (%lf)", 2 * (int)sizeof(x), v, (double)x);
return s;
}
};
template<>
struct ToString<int>
{
static std::string str(int i) {
return std::to_string(i);
}
};
template<typename... Ts> requires(to_string<Ts> && ...)
class Checker
{
public:
Checker(): m_success(true) {}
void vars(std::initializer_list<char const *> names) {
m_names = std::vector(names);
}
void values(Ts... values) {
m_values = std::make_tuple(values...);
}
bool check(bool b, char const *expr) {
if(!b) {
m_success = false;
fprintf(stderr, "FAILED: %s\n", expr);
printValues(std::index_sequence_for<Ts...> {});
}
return b;
}
bool successful() const {
return m_success;
}
private:
/* Iterate on `m_names` and `m_values` following the index sequence Is; for
each index, prints the variable name and value. */
template<std::size_t... Is>
void printValues(std::index_sequence<Is...>) {
(fprintf(stderr, " %s: %s\n", m_names[Is],
ToString<typename std::tuple_element<Is, decltype(m_values)>::type>
::str(std::get<Is>(m_values)).c_str()), ...);
}
std::vector<char const *> m_names;
std::tuple<Ts...> m_values;
bool m_success;
};
template<typename T> requires(to_string<T>)
bool runWithChecker(std::function<void(T, Checker<T> &)> f)
{
Checker<T> c;
runOnSampleInputs<T>([f, &c](T x) { f(x, c); });
return c.successful();
}
template<typename T, typename U> requires(to_string<T> && to_string<U>)
bool runWithChecker(std::function<void(T, U, Checker<T, U> &)> f)
{
Checker<T, U> c;
runOnSampleInputs<T, U>([f, &c](T x, U y) { f(x, y, c); });
return c.successful();
}

174
libnum/test/unit_sample.h Normal file
View File

@ -0,0 +1,174 @@
//---------------------------------------------------------------------------//
// ," /\ ", Azur: A game engine for CASIO fx-CG and PC //
// | _/__\_ | Designed by Lephe' and the Planète Casio community. //
// "._`\/'_." License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// unit_sample.h: Utility for generating sample values for testing
//
// This file mainly provides the runOnSampleInputs() function which runs a
// boolean function f() on a series of generated inputs. It achieves this by
// reading a storage of pre-computed inputs for each of the arguments of f.
// This requires a storage to exist for each argument's type, which is provided
// by SampleBase and subclassed by Sample.
//
// The meat of the generation is the generateIntSample() function, which
// generates sparse sets of integers of varied size that omit needlessly
// redundant input while densely covering special cases.
//---
#pragma once
#include <vector>
#include <type_traits>
#include <functional>
#include <cassert>
#include <stdint.h>
#include <num/num.h>
using namespace num;
//---
// Integer sampling
//---
template<typename T, typename Uint>
void generateIntSample(std::vector<T> &v, std::function<T(Uint)> ctor,
Uint start, Uint length, int count)
{
/* When we get to a set of size 16, do all values. */
if(count <= 16) {
/* If we have an even-only range, force in some odd numbers too. */
bool switch_odd = (length >= (Uint)(2*count));
Uint step = length / count;
while(count-- > 0) {
v.push_back(ctor(start));
start += switch_odd ? step ^ (count & 1) : step;
}
return;
}
/* Otherwise, fractally divide into 16 segments and assign a portion of the
available points to each section. We divide the count of points in 16ths
so that there is at least one point in each segment. */
Uint sublength = length / 16;
int subcount = count / 16;
/* This array must add up to 16. */
int props[16] = { 4, 1, 1, 0, 0, 2, 0, 0, 1, 0, 2, 0, 0, 0, 1, 4 };
for(int i = 0; i < 16; i++) {
if(props[i])
generateIntSample(v, ctor, (Uint)(start + i * sublength),
sublength, subcount * props[i]);
}
}
//---
// Input sampling
//---
template<typename T>
struct SampleBase {
static std::vector<T> v;
};
template<typename T>
std::vector<T> SampleBase<T>::v;
template<typename T>
struct Sample {};
template<typename T>
concept has_sample = requires { Sample<T>::get; };
template<>
struct Sample<num8>: SampleBase<num8>
{
static std::vector<num8> const &get() {
if(v.size() > 0)
return v;
for(int i = 0; i <= 0xff; i++) {
num8 x;
x.v = i;
v.push_back(x);
}
return v;
}
};
template<>
struct Sample<num16>: SampleBase<num16>
{
static std::vector<num16> const &get() {
if(v.size() > 0)
return v;
auto f = [](uint16_t i) { num16 x; x.v = i; return x; };
generateIntSample<num16, uint16_t>(v, f, 0, 1 << 15, 512);
generateIntSample<num16, uint16_t>(v, f, -(1 << 15), 1 << 15, 512);
return v;
}
};
template<>
struct Sample<num32>: SampleBase<num32>
{
static std::vector<num32> const &get() {
if(v.size() > 0)
return v;
auto f = [](uint32_t i) { num32 x; x.v = i; return x; };
generateIntSample<num32, uint32_t>(v, f, 0, 1 << 15, 512);
generateIntSample<num32, uint32_t>(v, f, -(1 << 15), 1 << 15, 512);
generateIntSample<num32, uint32_t>(v, f, 0, 1ul << 31, 512);
generateIntSample<num32, uint32_t>(v, f, 1ul << 31, 1ul << 31, 512);
return v;
}
};
template<>
struct Sample<int>: SampleBase<int>
{
static std::vector<int> const &get() {
if(v.size() > 0)
return v;
auto f = [](uint32_t i) { return i; };
generateIntSample<int, uint32_t>(v, f, 0, 1 << 15, 512);
generateIntSample<int, uint32_t>(v, f, -(1 << 15), 1 << 15, 512);
generateIntSample<int, uint32_t>(v, f, 0, 1ul << 31, 512);
generateIntSample<int, uint32_t>(v, f, 1ul << 31, 1ul << 31, 512);
return v;
}
};
template<>
struct Sample<num64>: SampleBase<num64>
{
static std::vector<num64> const &get() {
if(v.size() > 0)
return v;
auto f = [](uint64_t i) { num64 x; x.v = i; return x; };
generateIntSample<num64, uint64_t>(v, f, 0, 1 << 15, 512);
generateIntSample<num64, uint64_t>(v, f, -(1 << 15), 1 << 15, 512);
generateIntSample<num64, uint64_t>(v, f, 0, 1ul << 31, 512);
generateIntSample<num64, uint64_t>(v, f, -(1ul << 31), 1ul << 31, 512);
generateIntSample<num64, uint64_t>(v, f, 0, 1ull << 63, 512);
generateIntSample<num64, uint64_t>(v, f, 1ull << 63, 1ull << 63, 512);
return v;
}
};
//---
// Automatic test functions
//---
template<typename T> requires(has_sample<T>)
void runOnSampleInputs(std::function<void(T)> f)
{
for(auto t: Sample<T>::get())
f(t);
}
template<typename T, typename U> requires(has_sample<T> && has_sample<U>)
void runOnSampleInputs(std::function<void(T, U)> f)
{
for(auto t: Sample<T>::get())
for(auto u: Sample<U>::get())
f(t, u);
}

131
libnum/test/unit_scalar.cpp Normal file
View File

@ -0,0 +1,131 @@
//---------------------------------------------------------------------------//
// ," /\ ", Azur: A game engine for CASIO fx-CG and PC //
// | _/__\_ | Designed by Lephe' and the Planète Casio community. //
// "._`\/'_." License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// unit_scalar.cpp: Unit tests for scalar arithmetic
#include <stdio.h>
#include "unit_sample.h"
#include "unit_check.h"
/* Automatically stringify expressions in arguments to Checker.check() */
#define CHECK(EXPR) check(EXPR, #EXPR)
/* Test equality of num values up to slight variations */
template<typename T> requires(is_num<T>)
bool isEqUpTo(T x, T y, int i)
{
return abs(x.v - y.v) <= i;
}
template<typename T>
bool runInternalArithmeticTest(int error)
{
return runWithChecker<T, T>([error](T x, T y, Checker<T, T> &c) {
c.vars({ "x", "y" });
c.values(x, y);
/* Surprisinly the cast back to integer in T() does saturation
arithmetic, so we can't require equality in case of overflow. */
double sum = double(x) + double(y);
if(sum >= T::minDouble && sum <= T::maxDouble)
c.CHECK(x + y == T(sum));
double diff = double(x) - double(y);
if(diff >= T::minDouble && diff <= T::maxDouble)
c.CHECK(x - y == T(diff));
double prod = double(x) * double(y);
if(prod >= T::minDouble && prod <= T::maxDouble)
c.CHECK(isEqUpTo(x * y, T(prod), error));
if(y != 0) {
double quot = double(x) / double(y);
if(quot >= T::minDouble && quot <= T::maxDouble)
c.CHECK(x / y == T(quot));
}
c.CHECK(y <= 0 || x < 0 || x % y < y);
c.CHECK((x < y) + (x == y) + (x > y) == 1);
c.CHECK((x <= y) - (x == y) + (x >= y) == 1);
});
}
template<typename T>
bool runIntegerComparisonTest()
{
return runWithChecker<T, int>([](T x, int i, Checker<T, int> &c) {
c.vars({ "x", "i" });
c.values(x, i);
c.CHECK((x < i) + (x == i) + (x > i) == 1);
c.CHECK((x <= i) - (x == i) + (x >= i) == 1);
});
}
int main(void)
{
bool success = true;
printf("Testing unary laws on num8...\n");
success &= runWithChecker<num8>([](num8 x, Checker<num8> &c) {
c.vars({ "x" });
c.values(x);
c.CHECK(num8(num16(x)) == x);
c.CHECK(num8(num32(x)) == x);
c.CHECK(num8(num64(x)) == x);
c.CHECK(-x + x == 0);
});
printf("Testing binary laws on num8...\n");
success &= runInternalArithmeticTest<num8>(0);
printf("Testing comparisons of num8 with integers...\n");
success &= runIntegerComparisonTest<num8>();
printf("Testing unary laws on num16...\n");
success &= runWithChecker<num16>([](num16 x, Checker<num16> &c) {
c.vars({ "x" });
c.values(x);
c.CHECK(num16(num8(x)).v == (x.v & 0xff));
c.CHECK(x < 0 || num16(num8(x)) == x % num16(1));
c.CHECK(num16(num32(x)) == x);
c.CHECK(num16(num64(x)) == x);
c.CHECK(-x + x == 0);
});
printf("Testing binary laws on num16...\n");
success &= runInternalArithmeticTest<num16>(1);
printf("Testing comparisons of num16 with integers...\n");
success &= runIntegerComparisonTest<num16>();
printf("Testing unary laws on num32...\n");
success &= runWithChecker<num32>([](num32 x, Checker<num32> &c) {
c.vars({ "x" });
c.values(x);
c.CHECK(num32(num8(x)).v == (x.v & 0x0000ff00));
c.CHECK(num32(num16(x)).v >> 8 == (int16_t)(x.v >> 8));
c.CHECK(num32(num64(x)) == x);
c.CHECK(-x + x == 0);
});
printf("Testing binary laws on num32...\n");
success &= runInternalArithmeticTest<num32>(1);
printf("Testing comparisons of num32 with integers...\n");
success &= runIntegerComparisonTest<num32>();
printf("Testing unary laws on num64...\n");
success &= runWithChecker<num64>([](num64 x, Checker<num64> &c) {
c.vars({ "x" });
c.values(x);
c.CHECK(num64(num8(x)).v == (x.v & 0x00000000ff000000ll));
c.CHECK(num64(num16(x)).v >> 24 == (int16_t)(x.v >> 24));
c.CHECK(num64(num32(x)).v >> 16 == (int32_t)(x.v >> 16));
c.CHECK(-x + x == num64(0));
});
return (success ? 0 : 1);
}

View File

@ -1,5 +1,11 @@
#include <num/num.h>
//---------------------------------------------------------------------------//
// ," /\ ", Azur: A game engine for CASIO fx-CG and PC //
// | _/__\_ | Designed by Lephe' and the Planète Casio community. //
// "._`\/'_." License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// unit_static.cpp: Compile-time evaluation tests
#include <num/num.h>
using namespace num;
static_assert(sizeof(num8) == 1);
@ -23,31 +29,16 @@ static_assert(num32(num16(-15)) == num32(-15));
static_assert(num64(num16(1)) == num64(1));
static_assert(num64(num16(-1)) == num64(-1));
/* Comparisons between num8 and int */
static_assert(num8(0) == 0);
static_assert(num8(0) != 1);
static_assert(num8(0.5) != 0);
static_assert(num8(0.5) != 1);
static_assert(num8(1) == 0); // overflow
static_assert(num8(1) != 1); // overflow
static_assert(!(num8(0) < 0));
static_assert(num8(0) < 1);
static_assert(!(num8(0.5) < 0));
static_assert(num8(0.5) < 1);
static_assert(num8(0) <= 0);
static_assert(num8(0) <= 1);
static_assert(!(num8(0.5) <= 0));
static_assert(num8(0.5) <= 1);
static_assert(!(num8(0) > 0));
static_assert(!(num8(0) > 1));
static_assert(num8(0.5) > 0);
static_assert(!(num8(0.5) > 1));
static_assert(num8(0) >= 0);
static_assert(!(num8(0) >= 1));
static_assert(num8(0.5) >= 0);
static_assert(!(num8(0.5) >= 1));