soft_double is a C++ header-only library that implements
a 64-bit double-precision floating-point data type in software.
The data type implemented is called ::math::softfloat::soft_double
and it can be used essentially like a regular
built-in 64-bit floating-point type (such as built-in double
).
This data type type is also aliased to ::math::softfloat::float64_t
.
Some C/C++ environments do not support a built-in 64-bit floating-point type,
which might be commonly known as double
or long
double
,
depending on the target system/compiler combination.
Consider, for instance, the avr-gcc
tool chain popularly used on
8-bit embedded systems like Arduino or standalone bare metal AVR.
Prior to version 10, this compiler does not support 64-bit double
.
On such systems, soft_double can be used to provide a software-emulated,
portable implementation of 64-bit double
.
soft_double implements common algebraic operations,
comparison operations, simple functions such as
fabs
, frexp
, sqrt
, some power functions such as
log
, exp
, a few trigonometric functions including
sin
, cos
, and more. There is also full support/specialization
of std::numeric_limits<soft_double>
for the soft_double
type.
The soft_double implementation is written in header-only C++14 and is compatible for C++14, 17, 20, 23 and beyond.
Using soft_double is straightforward. Simply #include <math/softfloat/soft_double.h>
and
the soft_double
type can be used almost like familiar built-in 64-bit double
.
For instance,
#include <math/softfloat/soft_double.h>
// Use a convenient alias for float64_t.
using ::math::softfloat::float64_t;
// Use the type like built-in `double`.
const float64_t one_third = float64_t(1U) / 3U;
An interesting detail in this code sample is the construction
of one_third
from the composite initialization
provided by float64_t(1U) / 3U
.
This is needed, for example, when the compiler
does not support 64-bit built-in double
or long
double
and these are limited to 32-bits (because
the 64-bit floating-point type is the actual type
that is being emulated). This situation arises,
as mentioned above, on certain popular versions of
the avr-gcc
and other tool chains.
- Clean header-only C++ design.
- Seamless portability to any modern C++14, 17, 20, 23 compiler (and beyond).
- Achieve efficiency suitable for bare-metal embedded systems.
- Particularly useful if 64-bit native
double
or a similar built-in type is unavailable. - Use refactored versions of trusted algorithms based on those found in softfloat 3e.
The following more detailed example provides an in-depth examination into effectively using soft_double. This code computes and checks the value of
This example, compiled with successful output result, is shown in its entirety in the following short link to godbolt.
#include <cmath>
#include <iomanip>
#include <iostream>
#include <math/softfloat/soft_double.h>
int main()
{
// Use a convenient alias for float64_t.
using ::math::softfloat::float64_t;
// Use a cached value for pi.
const float64_t my_pi = float64_t::my_value_pi();
// Compute soft_double sqrt(pi).
const float64_t s = sqrt(my_pi);
using std::sqrt;
// Compare with native double sqrt(pi).
const bool result_is_ok =
(s.crepresentation() == float64_t(sqrt(3.1415926535897932384626433832795028841972)).crepresentation());
std::cout << "result_is_ok: " << std::boolalpha << result_is_ok << std::endl;
}
When working with soft_double, performing library verification,
or prototyping project-specific software algorithms
(intended) to use the soft_double
type, a built-in 64-bit double
can be helpful if available.
The examples, testing and verification of numerical correctness
in the soft_double project do, in fact, actually use 64-bit built-in double
.
soft_double can readily be used on the metal to emulate a 64-bit floating point type. This can be provide the ability to do 64-bit floating-point calculations on target system/compiler combinations lacking a built-in 64-bit floating-point type.
An example tested on various microcontrollers system
can be found in the file
example010_hypergeometric_2f1.cpp
.
This benchmark has also been tested on the 8-bit MICROCHIP ATmega328P controller
with avr-gcc
versions 5 and 7 (both lacking built-in 64-bit double
).
When working with even the most tiny microcontroller systems, I/O streaming can optionally be disabled with the compiler switch:
#define SOFT_DOUBLE_DISABLE_IOSTREAM
Various interesting and algorithmically challenging examples have been implemented. It is hoped that the examples provide inspiration and guidance on how to use soft_double.
- computes a square root.
- implements cylindrical Bessel functions of integral order via downward recursion with a Neumann sum.
- performs a small-argument polylogarithm series calculation.
-
computes
$\sim 15$ decimal digits of Catalan's constant using an accelerated series. -
calculates
$\sim 15$ decimal digits of a hypergeometric function value using a classic iterative rational approximation scheme. - uses trapezoid integration with an integral representation involving locally-written trigonometric sine and cosine functions to compute several cylindrical Bessel function values.
-
verifies that C++20
constexpr
-ness works properly for both rudimentary assignment-operation as well as an elementary square root function.
When using C++20, soft_double
supports compile-time
constexpr
construction and evaluation of results
of binary arithmetic, comparison operators
and various elementary functions.
The following code, for instance, shows a compile-time instantiation
of soft_double
with subsequent constexpr
evaluation
of a square root function and its comparison of its result
with the known control value.
See this example fully worked out at the following
short link to godbolt.
The generated assembly includes nothing other than the call to main()
and its subsequent return
of the value zero
(i.e., main()
's successful return-value in this example).
#include <cmath>
#include <math/softfloat/soft_double.h>
// Use a C++20 compiler for this example.
int main()
{
// Use a convenient alias for float64_t.
using ::math::softfloat::float64_t;
// Use a cached value for pi.
constexpr float64_t my_pi = float64_t::my_value_pi();
// Compute soft_double sqrt(pi).
constexpr float64_t s = sqrt(my_pi);
constexpr auto result_is_ok =
(s.crepresentation() == static_cast<typename float64_t::representation_type>(UINT64_C(0x3FFC5BF891B4EF6A)));
// constexpr verification.
// This is a compile-time comparison.
static_assert(result_is_ok, "Error: example001_roots_sqrt not OK!");
return (result_is_ok ? 0 : -1);
}
constexpr
-ness of soft_double
has been checked on GCC, clang
(with -std=c++20
and beyond) and VC 14.2 and higher (with /std:c++latest
),
also for various embedded compilers such as avr-gcc
11 and up,
arm-non-eabi-gcc
11 and up, and more.
In addition, less modern compiler versions have been sporadically
(not exhaustively) checked for constexpr
usage of soft_double
.
If you have an older compiler, you might have to check the compiler's
ability to obtain the entire benefit of constexpr
with soft_double
.
In issue 110,
the topic of constexpr
-ness regarding construction from built-in float
,
double
and long
double
was briefly addressed. At the moment,
construction from built-in floating-point types adheres to C++20 constexpr
-ness
or higher. Perhaps in the future, an alternate programing could bring this feature
to earlier language standards.
The macro sequence below can be used to test for the feature
of constexpr
-ness regarding construction from built-in
floating-point types.
#include <math/softfloat/soft_double.h>
#if ((defined SOFT_DOUBLE_CONSTEXPR_BUILTIN_FLOATS) && (SOFT_DOUBLE_CONSTEXPR_BUILTIN_FLOATS == 1))
#endif
It is not mandatory to actually use this feature-test if the
language standard being used is known to be sufficiently high
for compatibility. The following code, for instance, uses
constexpr
construction from built-in double
,
as shown also in this
short link to godbolt.
#include <math/softfloat/soft_double.h>
auto main() -> int
{
using ::math::softfloat::float64_t;
constexpr auto gravitational_constant = float64_t { 6.674e-11 };
constexpr auto near_pi_constant = float64_t { 3.14 };
constexpr auto one_quarter_constant = float64_t { 0.25 };
static_assert(gravitational_constant < 1, "Error: Initialization constexpr-double does not properly work");
static_assert(gravitational_constant != near_pi_constant, "Error: Initialization constexpr-double does not properly work");
static_assert(static_cast<int>(INT8_C(4)) * one_quarter_constant == 1, "Error: Initialization constexpr-double does not properly work");
static_assert(static_cast<int>(INT8_C(12)) * one_quarter_constant < near_pi_constant, "Error: Initialization constexpr-double does not properly work");
static_assert(static_cast<int>(INT8_C(13)) * one_quarter_constant > near_pi_constant, "Error: Initialization constexpr-double does not properly work");
}
The recent status of building and executing the tests and examples in Continuous Integration (CI) is always shown in the Build Status banner.
Simply using the soft_double.h
header
can be accomplished by identifying the header within
its directory, including the header path and header in the normal C++ way.
Building and running the tests and examples can be accomplished
using the Microsoft VisualStudio solution workspace provided
in soft_double.sln
located in the project's root directory.
It is also possible, if desired, to build and execute
the tests and examples using various different OS/compiler
combinations.
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.
It uses easy-to-understand subroutines called from
main()
that exercise various test cases.
Furthermore,
the example010_hypergeometric_2f1.cpp
benchmark is built-for the 8-bit MICROCHIP ATmega328P controller
and it is also built-for and executed-on the simulated
32-bit ARM(R) Cortex(R)-M4F in QEMU in CI.
CI runs on push and pull request using GitHub Actions. Various compilers, operating systems, and various C++ standards are included in CI.