Multiprecision arithmetic is not required. Within floating point arithmetic that uses less than 64 bits for the significand (aka mantissa) division by nmax=std::numeric_limits<uint64_t>::max()
can be computed in an exactly rounded way (i.e. the result of the computation will be identical to the closest approximation of the exact arithmetic ratio in the target floating point format) as follows:
n/nmax
= n/(264-1)
= n/264/(1-2-64)
= n/264*(1+2-64+2-128+...)
= n/264 + whatever doesn't fit in the significand
Thus the result is
n/nmax = n/264
The following C++ test program implements both the naive and accurate methods of computing the ratio n/nmax:
#include <climits>
#include <cmath>
#include <iostream>
#include <limits>
#include <type_traits>
template<typename F, typename U>
F map_to_unit_range_naive(U n)
{
static_assert(std::is_floating_point<F>::value, "Result type must be a floating point type");
static_assert(std::is_unsigned<U>::value, "Input type must be an unsigned integer type");
return F(n)/F(std::numeric_limits<U>::max());
}
template<typename F, typename U>
F map_to_unit_range_accurate(U n)
{
static_assert(std::is_floating_point<F>::value, "Result type must be a floating point type");
static_assert(std::is_unsigned<U>::value, "Input type must be an unsigned integer type");
const int UBITS = sizeof(U) * CHAR_BIT;
return std::ldexp(F(n), -UBITS);
}
template<class F, class U>
double error_mapping_to_unit_range(U n)
{
const F r1 = map_to_unit_range_accurate<F>(n);
const F r2 = map_to_unit_range_naive<F>(n);
return (1-r2/r1);
}
#define CHECK_MAPPING_TO_UNIT_RANGE(n, result_type) \
std::cout << "map_to_unit_range<" #result_type ">(" #n "): err=" \
<< error_mapping_to_unit_range<result_type>(n)*100 << "%" \
<< std::endl;
int main()
{
CHECK_MAPPING_TO_UNIT_RANGE(123u, float);
CHECK_MAPPING_TO_UNIT_RANGE(123ul, float);
CHECK_MAPPING_TO_UNIT_RANGE(1234567890u, float);
CHECK_MAPPING_TO_UNIT_RANGE(1234567890ul, float);
std::cout << "\n";
CHECK_MAPPING_TO_UNIT_RANGE(123ul, double);
CHECK_MAPPING_TO_UNIT_RANGE(1234567890ul, double);
return 0;
}
The program demonstrates that the naive method is on par with the carefully crafted code:
map_to_unit_range<float>(123u): err=0%
map_to_unit_range<float>(123ul): err=0%
map_to_unit_range<float>(1234567890u): err=0%
map_to_unit_range<float>(1234567890ul): err=0%
map_to_unit_range<double>(123ul): err=0%
map_to_unit_range<double>(1234567890ul): err=0%
This may seem surprising at first, but it has a simple explanation - if the floating point type cannot represent the integral value 2N-1 exactly, then it rounds it to 2N, effectively resulting in an accurate division on the next step (according to the above formula).
Note that when the precision of the floating point type exceeds the size of the integer type (so that 2N-1 can be represented exactly) the premises for the formula are not met, and the "accurate" method stops being such:
int main()
{
CHECK_MAPPING_TO_UNIT_RANGE(123u, double);
CHECK_MAPPING_TO_UNIT_RANGE(1234567890u, double);
return 0;
}
Output:
map_to_unit_range<double>(123u): err=-2.32831e-08%
map_to_unit_range<double>(1234567890u): err=-2.32831e-08%
The "error" here is coming from the "accurate" method.
Credits:
Many thanks to @interjay and @Jonathan Mee for their thorough peer review of the previous versions of this answer.