Work around numbers with very large first factors

We introduce the `known_first_factor` variable template.
This commit is contained in:
Chip Hogg
2022-03-19 15:55:34 +00:00
parent a99e5f9032
commit 166dd1e944
2 changed files with 48 additions and 2 deletions

View File

@@ -28,6 +28,7 @@
#include <concepts>
#include <cstdint>
#include <numbers>
#include <optional>
#include <stdexcept>
namespace units {
@@ -282,6 +283,20 @@ constexpr bool is_valid_base_power(const BasePower auto& bp)
}
if constexpr (std::is_same_v<decltype(bp.get_base()), std::intmax_t>) {
// Some prime numbers are so big, that we can't check their primality without exhausting limits on constexpr steps
// and/or iterations. We can still _perform_ the factorization for these by using the `known_first_factor`
// workaround. However, we can't _check_ that they are prime, because this workaround depends on the input being
// usable in a constexpr expression. This is true for `prime_factorization` (below), where the input `N` is a
// template parameter, but is not true for our case, where the input `bp.get_base()` is a function parameter. (See
// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1045r1.html for some background reading on this
// distinction.)
//
// In our case: we simply give up on excluding every possible ill-formed base power, and settle for catching the
// most likely and common mistakes.
if (const bool too_big_to_check = (bp.get_base() > 1'000'000'000)) {
return true;
}
return is_prime(bp.get_base());
} else {
return bp.get_base() > 0;
@@ -468,12 +483,30 @@ constexpr auto operator/(Magnitude auto l, Magnitude auto r) { return l * pow<-1
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// `as_magnitude()` implementation.
// Sometimes we need to give the compiler a "shortcut" when factorizing large numbers (specifically, numbers whose
// _first factor_ is very large). If we don't, we can run into limits on the number of constexpr steps or iterations.
//
// To provide the first factor for a given number, specialize this variable template.
//
// WARNING: The program behaviour will be undefined if you provide a wrong answer, so check your math!
template<std::intmax_t N>
inline constexpr std::optional<std::intmax_t> known_first_factor = std::nullopt;
namespace detail {
// Helper to perform prime factorization at compile time.
template<std::intmax_t N>
requires(N > 0)
struct prime_factorization {
static constexpr std::intmax_t first_base = static_cast<std::intmax_t>(Factorizer::find_first_factor(N));
static constexpr std::intmax_t get_or_compute_first_factor()
{
if constexpr (known_first_factor<N>.has_value()) {
return known_first_factor<N>.value();
} else {
return static_cast<std::intmax_t>(Factorizer::find_first_factor(N));
}
}
static constexpr std::intmax_t first_base = get_or_compute_first_factor();
static constexpr std::intmax_t first_power = multiplicity(first_base, N);
static constexpr std::intmax_t remainder = remove_power(first_base, first_power, N);

View File

@@ -28,6 +28,9 @@
using namespace units;
using namespace units::detail;
template<>
inline constexpr std::optional<std::intmax_t> units::known_first_factor<9223372036854775783> = 9223372036854775783;
namespace {
// A set of non-standard bases for testing purposes.
@@ -149,7 +152,17 @@ TEST_CASE("make_ratio performs prime factorization correctly")
// all odd numbers up to sqrt(N), will exceed this limit for the following prime. Thus, for this test to pass, we
// need to be using a more efficient algorithm. (We could increase the limit, but we don't want users to have to
// mess with compiler flags just to compile the code.)
as_magnitude<334524384739>();
as_magnitude<334'524'384'739>();
}
SECTION ("Can bypass computing primes by providing known_first_factor<N>") {
// Sometimes, even wheel factorization isn't enough to handle the compilers' limits on constexpr steps and/or
// iterations. To work around these cases, we can explicitly provide the correct answer directly to the compiler.
//
// In this case, we test that we can represent the largest prime that fits in a signed 64-bit int. The reason this
// test can pass is that we have provided the answer, by specializing the `known_first_factor` variable template
// above in this file.
as_magnitude<9'223'372'036'854'775'783>();
}
}