Wide-decimal implements a generic C++ template for extended precision decimal float types.
This C++ template header-only library implements drop-in big decimal float types,
such as dec51_t
, dec101_t
, dec1001_t
, dec10001_t
, dec1000001_t
, etc.,
that can be used essentially like regular built-in floating-point types.
Wide-decimal supports decimal float types having digit counts ranging
roughly from about
.
Wide-decimal implements both common algebraic operations as well as
a few common <cmath>
-like functions such as fabs
, sqrt
and log
,
and also includes full support for std::numeric_limits
.
Wide-decimal is written in header-only C++11, and compatible through C++11, 14, 17, 20.
- Wide precision range up to one million decimal digits
- Moderately good efficiency over the entire wide precision range
- Clean header-only C++11 design
- Seamless portability to any modern C++11, 14, 17, 20 compiler
- Scalability with small memory footprint and efficiency suitable for bare-metal embedded systems
Easy application follows via a traditional C-style typedef or C++11 alias. The defined type can be used very much like a built-in floating-point type.
The following sample, for instance, defines and uses a decimal type
called dec101_t
having
decimal digits of precision.
The subroutine do_something()
initializes the variable d
of type dec101_t
with 1/3 and subsequently prints the 101 digit
value of d
to the console.
In particular,
#include <iomanip>
#include <iostream>
#include <math/wide_decimal/decwide_t.h>
void do_something()
{
using dec101_t = math::wide_decimal::decwide_t<INT32_C(101), std::uint32_t, void>;
dec101_t d = dec101_t(1) / 3;
// 0.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
std::cout << std::setprecision(std::numeric_limits<dec101_t>::digits) << d << std::endl;
}
The local data type dec101_t
is defined with a C++11 alias.
The first template parameter INT32_C(101)
sets the decimal digit
count while the second optional template parameter std::uint32_t
sets the internal limb type. If the second template parameter is left blank,
the default limb type is 32 bits in width and unsigned.
The template signature of the decwide_t
class is shown below.
template<const std::int32_t ParamDigitsBaseTen,
typename LimbType = std::uint32_t,
typename AllocatorType = std::allocator<void>,
typename InternalFloatType = double,
typename ExponentType = std::int64_t,
typename FftFloatType = double>
class decwide_t;
decwide_t
also has a third (and a few more) optional template paramter(s).
The third template parameter
can be used to set the allocator type for internal storage of the
wide decimal type. The default allocator type is
std::allocator<limb_type>
which helps to reduce
stack consumption, especially when using higher digit counts.
If low digit counts are used, the allocator type can
be explicitly set to void
and stack allocation is used with an array
-like
internal representation.
Various interesting and algorithmically challenging examples have been implemented. It is hoped that the examples provide inspiration and guidance on how to use wide-decimal.
- performa a check of shoolbook multiplication using a small digit range and small 8-bit limb.
- performs a hard-coded multiplication check resulting in .
- computes a square root.
- computes a seventh root.
- uses roots and algebraic operations to compute decimal digits of a fascinating Pisot number that is almost integer.
- computes a square root with a wide decimal representation having 8-bit limbs.
- verifies a list of values with .
- calculates decimal digits of using a Gauss AGM iteration.
- calculates decimal digits of using a 16-bit internal limb type.
- calculates decimal digits of .
- calculates decimal digits of using a Borwein quintic iteration.
- calculates yet again decimal digits of using an 8-bit internal limb type and
float
internal floating-point type. - computes a Riemann zeta function value.
- implements cylindrical Bessel functions of integral order via downward recursion with a Neumann sum.
- performs a small-argument polylogarithm series calculation.
- calculates the value of a logarithm (internally using a Gauss AGM method).
- computes decimal digits of Catalan's constant using an accelerated series.
- implements
tgamma(x)
using Stirling's asymptotic expansion of the logarithm of the Gamma function with Bernoulli numbers and subsequently calculates decimal digits of for small integer . - checks basic compatibility of standalone
decwide_t
withBoost.Math
by testing a cube root value obtained fromboost::math::cbrt
. - also checks standalone
decwide_t
with significantly more ofBoost.Math
by testing a digit generalized Legendre function value (usingboost::math::tgamma
and more to do so). - checks yet again standalone
decwide_t
withBoost.Math
's availableboost::math::tgamma
function for small-ish decimal floats having decimal digits. - calculates a decimal digit hypergeometric function value using an iterative rational approximation scheme.
- calculates another decimal digit hypergeometric function in a similar fashion.
- uses trapezoid integration with an integral representation involving locally-written trigonometric sine and cosine functions to compute several cylindrical Bessel function values.
- verifies the proper representation of a wide selection of small-valued, pure integral rational quotients.
- and exercise calculations that also run on tiny bare-metal embedded systems, featuring a digit square root calculation and a digit AGM iteration for .
Testing is a big issue and a growing test suite is in continued progress
providing for tested, efficient functionality on the PC and workstation.
The GitHub code is delivered with an affiliated MSVC project that uses easy-to-understand
subroutines called from main()
that exercise various test cases.
Continuous integration runs on push using GitHub Actions. Various compilers, operating systems, and C++ standards ranging from C++11, 14, 17, 20 are included in CI.
Wide-Decimal has been tested with numerous compilers for target systems ranging from 8 to 64 bits. The library is specifically designed for modest efficiency (not the world's fastest) over the entire range of small to large digit counts. How efficient is the code? Based on a very general comparison, a million digit AGM calculation of is about five times slower than an equivalent calculation performed with a big float data type based on GMP.
Portability of the code is another key point of focus. Special care has been taken to test in certain high-performance embedded real-time programming environments.
When working with even the most tiny microcontroller systems, various heavy-wieght features such as I/O streaming, dynamic memory allocation, construction from character string and caching constant values such as and can optionally be disabled with the compiler switches:
#define WIDE_DECIMAL_DISABLE_IOSTREAM
#define WIDE_DECIMAL_DISABLE_DYNAMIC_MEMORY_ALLOCATION
#define WIDE_DECIMAL_DISABLE_CONSTRUCT_FROM_STRING
#define WIDE_DECIMAL_DISABLE_CACHED_CONSTANTS
#define WIDE_DECIMAL_DISABLE_USE_STD_FUNCTION
#define WIDE_DECIMAL_NAMESPACE
Each one of these compiler switches has an intuitive name intended to represent its meaning.
Note: Activating the option WIDE_DECIMAL_DISABLE_DYNAMIC_MEMORY_ALLOCATION
simultaneously disallows using decwide_t
in a multithreaded application.
So if PC-based or other kinds of multithreading are used, then dynamic memory
allocation is needed and can not be disabled. In other words,
// Activate/Deactivate the disable of dynamic memory.
// For most multithreaded PC work, comment out or remove
// this line entirely (i.e., thereby enable dynamic memory).
#define WIDE_DECIMAL_DISABLE_DYNAMIC_MEMORY_ALLOCATION
Let's consider also the macro WIDE_DECIMAL_NAMESPACE
in greater detail.
#define WIDE_DECIMAL_NAMESPACE
This is an advanced macro intended to be used in strict, exacting applications for which
using the unqualified, global namespace math
(i.e., namespace
::math
) is undesired or inacceptable.
We recall that all parts of the wide-decimal implementation,
such as the decwide_t
class and its associated implementation
details reside within namespace
::math::wide_decimal
Defining the macro WIDE_DECIMAL_NAMESPACE
to be something like,
for instance,
-DWIDE_DECIMAL_NAMESPACE=something_unique
places all parts of the wide-decimal's uintwide_t
template class implementation
and its associated details within the prepended outer namespace
something_unique
--- as in
namespace something_unique::math::wide_decimal
{
// ...
}
When utilizing the WIDE_DECIMAL_NAMESPACE
option,
vary the actual name or nesting depth of the desired prepended
outer namespace if/as needed for your particular project.
By default the macro WIDE_DECIMAL_NAMESPACE
is not defined.
In this default state, namespace
::math::wide_decimal
is used
and the decwide_t
class and its associated implementation
details reside therein.
The example below calculates the square root of the decimal representation of , the result of which is approximately .
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <math/wide_decimal/decwide_t.h>
auto main() -> int
{
using local_limb_type = std::uint16_t;
using dec101_t = math::wide_decimal::decwide_t<INT32_C(101), local_limb_type, void>;
const dec101_t s = sqrt(dec101_t(123456U) / 100);
// N[Sqrt[123456/100], 101]
const dec101_t control
{
"35.136306009596398663933384640418055759751518287169314528165976164717710895452890928635031219132220978"
};
const dec101_t closeness = fabs(1 - (s / control));
const auto result_is_ok = (closeness < (std::numeric_limits<dec101_t>::epsilon() * 10));
std::cout << "result_is_ok: " << std::boolalpha << result_is_ok << std::endl;
}
In the following code, we compute (one million and one) decimal digits of the fundamental constant . The truncated (non-rounded) expected result is . In this particular example, all heavy-weight components are deactivated and this particular calculation is, in fact, suitable for a bare-metal mega-digit pi calculation.
In this example, note how a specialized custom allocator called
util::n_slot_array_allocator
is utilized for exact, efficient memory management
of a certain number of temporary storages of mega-digit numbers
(tuned to 18 in this particular example).
#include <iomanip>
#include <iostream>
#define WIDE_DECIMAL_DISABLE_IOSTREAM
#define WIDE_DECIMAL_DISABLE_DYNAMIC_MEMORY_ALLOCATION
#define WIDE_DECIMAL_DISABLE_CONSTRUCT_FROM_STRING
#define WIDE_DECIMAL_DISABLE_CACHED_CONSTANTS
#include <math/wide_decimal/decwide_t.h>
#include <util/memory/util_n_slot_array_allocator.h>
auto main() -> int
{
using local_limb_type = std::uint32_t;
constexpr std::int32_t wide_decimal_digits10 = INT32_C(1000001);
constexpr std::int32_t local_elem_number =
math::wide_decimal::detail::decwide_t_helper<wide_decimal_digits10, local_limb_type>::elem_number;
constexpr std::int32_t local_elem_digits10 =
math::wide_decimal::detail::decwide_t_helper<wide_decimal_digits10, local_limb_type>::elem_digits10;
using local_allocator_type = util::n_slot_array_allocator<void, local_elem_number, 18U>;
using local_wide_decimal_type =
math::wide_decimal::decwide_t<wide_decimal_digits10, local_limb_type, local_allocator_type, double>;
const auto start = std::clock();
const local_wide_decimal_type my_pi =
math::wide_decimal::pi<wide_decimal_digits10, local_limb_type, local_allocator_type, double>(nullptr);
const auto stop = std::clock();
std::cout << "Time example002_pi(): "
<< (float) (stop - start) / (float) CLOCKS_PER_SEC
<< std::endl;
const auto head_is_ok = std::equal(my_pi.crepresentation().cbegin(),
my_pi.crepresentation().cbegin() + math::constants::const_pi_control_head_32.size(),
math::constants::const_pi_control_head_32.begin());
using const_iterator_type = typename local_wide_decimal_type::array_type::const_iterator;
const_iterator_type
fi
(
my_pi.crepresentation().cbegin()
+ static_cast<std::uint32_t>
(
static_cast<std::uint32_t>(1UL + ((wide_decimal_digits10 - 1UL) / local_elem_digits10))
- static_cast<std::uint32_t>(math::constants::const_pi_control_tail_32_1000001.size())
)
);
const auto tail_is_ok = std::equal(fi,
fi + math::constants::const_pi_control_tail_32_1000001.size(),
math::constants::const_pi_control_tail_32_1000001.begin());
const auto result_is_ok = (head_is_ok && tail_is_ok);
std::cout << "result_is_ok: " << std::boolalpha << result_is_ok << std::endl;
}
The million digit run is comparatively slow and requires approximately 10 seconds on a modern PC. Considering, however, the design goals of header-only and capable of running on bare-metal, this is a very nice calculational result.
The wide-decimal float back end is used to compute decimal digits of the mathematical constant on selected bare-metal OS-less microcontroller systems in pi-crunch-metal