Skip to content

Commit

Permalink
Use dragon in constexpr
Browse files Browse the repository at this point in the history
  • Loading branch information
vitaut committed May 23, 2023
1 parent a54cb10 commit 858e528
Show file tree
Hide file tree
Showing 4 changed files with 9 additions and 238 deletions.
191 changes: 3 additions & 188 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,7 @@ constexpr uint32_t basic_data<T>::fractional_part_rounding_thresholds[];
// This is a struct rather than an alias to avoid shadowing warnings in gcc.
struct data : basic_data<> {};

// DEPRECATED!
// Returns a cached power of 10 `c_k = c_k.f * pow(2, c_k.e)` such that its
// (binary) exponent satisfies `min_exponent <= c_k.e <= min_exponent + 28`.
FMT_CONSTEXPR inline fp get_cached_power(int min_exponent,
Expand Down Expand Up @@ -2830,78 +2831,6 @@ FMT_INLINE FMT_CONSTEXPR bool signbit(T value) {
return std::signbit(static_cast<double>(value));
}

enum class round_direction { unknown, up, down };

// Given the divisor (normally a power of 10), the remainder = v % divisor for
// some number v and the error, returns whether v should be rounded up, down, or
// whether the rounding direction can't be determined due to error.
// error should be less than divisor / 2.
FMT_CONSTEXPR inline round_direction get_round_direction(uint64_t divisor,
uint64_t remainder,
uint64_t error) {
FMT_ASSERT(remainder < divisor, ""); // divisor - remainder won't overflow.
FMT_ASSERT(error < divisor, ""); // divisor - error won't overflow.
FMT_ASSERT(error < divisor - error, ""); // error * 2 won't overflow.
// Round down if (remainder + error) * 2 <= divisor.
if (remainder <= divisor - remainder && error * 2 <= divisor - remainder * 2)
return round_direction::down;
// Round up if (remainder - error) * 2 >= divisor.
if (remainder >= error &&
remainder - error >= divisor - (remainder - error)) {
return round_direction::up;
}
return round_direction::unknown;
}

namespace digits {
enum result {
more, // Generate more digits.
done, // Done generating digits.
error // Digit generation cancelled due to an error.
};
}

struct gen_digits_handler {
char* buf;
int size;
int precision;
int exp10;
bool fixed;

FMT_CONSTEXPR digits::result on_digit(char digit, uint64_t divisor,
uint64_t remainder, uint64_t error,
bool integral) {
FMT_ASSERT(remainder < divisor, "");
buf[size++] = digit;
if (!integral && error >= remainder) return digits::error;
if (size < precision) return digits::more;
if (!integral) {
// Check if error * 2 < divisor with overflow prevention.
// The check is not needed for the integral part because error = 1
// and divisor > (1 << 32) there.
if (error >= divisor || error >= divisor - error) return digits::error;
} else {
FMT_ASSERT(error == 1 && divisor > 2, "");
}
auto dir = get_round_direction(divisor, remainder, error);
if (dir != round_direction::up)
return dir == round_direction::down ? digits::done : digits::error;
++buf[size - 1];
for (int i = size - 1; i > 0 && buf[i] > '9'; --i) {
buf[i] = '0';
++buf[i - 1];
}
if (buf[0] > '9') {
buf[0] = '1';
if (fixed)
buf[size++] = '0';
else
++exp10;
}
return digits::done;
}
};

inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) {
// Adjust fixed precision by exponent because it is relative to decimal
// point.
Expand All @@ -2910,101 +2839,6 @@ inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) {
precision += exp10;
}

// Generates output using the Grisu digit-gen algorithm.
// error: the size of the region (lower, upper) outside of which numbers
// definitely do not round to value (Delta in Grisu3).
FMT_INLINE FMT_CONSTEXPR20 auto grisu_gen_digits(fp value, uint64_t error,
int& exp,
gen_digits_handler& handler)
-> digits::result {
const fp one(1ULL << -value.e, value.e);
// The integral part of scaled value (p1 in Grisu) = value / one. It cannot be
// zero because it contains a product of two 64-bit numbers with MSB set (due
// to normalization) - 1, shifted right by at most 60 bits.
auto integral = static_cast<uint32_t>(value.f >> -one.e);
FMT_ASSERT(integral != 0, "");
FMT_ASSERT(integral == value.f >> -one.e, "");
// The fractional part of scaled value (p2 in Grisu) c = value % one.
uint64_t fractional = value.f & (one.f - 1);
exp = count_digits(integral); // kappa in Grisu.
// Non-fixed formats require at least one digit and no precision adjustment.
if (handler.fixed) {
adjust_precision(handler.precision, exp + handler.exp10);
// Check if precision is satisfied just by leading zeros, e.g.
// format("{:.2f}", 0.001) gives "0.00" without generating any digits.
if (handler.precision <= 0) {
if (handler.precision < 0) return digits::done;
// Divide by 10 to prevent overflow.
uint64_t divisor = data::power_of_10_64[exp - 1] << -one.e;
auto dir = get_round_direction(divisor, value.f / 10, error * 10);
if (dir == round_direction::unknown) return digits::error;
handler.buf[handler.size++] = dir == round_direction::up ? '1' : '0';
return digits::done;
}
}
// Generate digits for the integral part. This can produce up to 10 digits.
do {
uint32_t digit = 0;
auto divmod_integral = [&](uint32_t divisor) {
digit = integral / divisor;
integral %= divisor;
};
// This optimization by Milo Yip reduces the number of integer divisions by
// one per iteration.
switch (exp) {
case 10:
divmod_integral(1000000000);
break;
case 9:
divmod_integral(100000000);
break;
case 8:
divmod_integral(10000000);
break;
case 7:
divmod_integral(1000000);
break;
case 6:
divmod_integral(100000);
break;
case 5:
divmod_integral(10000);
break;
case 4:
divmod_integral(1000);
break;
case 3:
divmod_integral(100);
break;
case 2:
divmod_integral(10);
break;
case 1:
digit = integral;
integral = 0;
break;
default:
FMT_ASSERT(false, "invalid number of digits");
}
--exp;
auto remainder = (static_cast<uint64_t>(integral) << -one.e) + fractional;
auto result = handler.on_digit(static_cast<char>('0' + digit),
data::power_of_10_64[exp] << -one.e,
remainder, error, true);
if (result != digits::more) return result;
} while (exp > 0);
// Generate digits for the fractional part.
for (;;) {
fractional *= 10;
error *= 10;
char digit = static_cast<char>('0' + (fractional >> -one.e));
fractional &= one.f - 1;
--exp;
auto result = handler.on_digit(digit, one.f, fractional, error, false);
if (result != digits::more) return result;
}
}

class bigint {
private:
// A bigint is stored as an array of bigits (big digits), with bigit at index
Expand Down Expand Up @@ -3505,7 +3339,7 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs,
int exp = 0;
bool use_dragon = true;
unsigned dragon_flags = 0;
if (!is_fast_float<Float>()) {
if (!is_fast_float<Float>() || is_constant_evaluated()) {
const auto inv_log2_10 = 0.3010299956639812; // 1 / log2(10)
using info = dragonbox::float_info<decltype(converted_value)>;
const auto f = basic_fp<typename info::carrier_uint>(converted_value);
Expand All @@ -3516,7 +3350,7 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs,
exp = static_cast<int>(
std::ceil((f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10));

This comment has been minimized.

Copy link
@phprus

phprus May 23, 2023

Contributor

@vitaut
Broken on macOS 13.3.1
Apple clang version 14.0.3 (clang-1403.0.22.14.1)
C++20.

/.../fmt/test/compile-fp-test.cc:32: error: call to consteval function 'test_format<11UL, char, float, FMT_COMPILE_STRING>' is not a constant expression
/.../fmt/test/compile-fp-test.cc:32:27: error: call to consteval function 'test_format<11UL, char, float, FMT_COMPILE_STRING>' is not a constant expression
  EXPECT_EQ("392.500000", test_format<11>(FMT_COMPILE("{0:f}"), 392.5f));
                          ^
/.../fmt/include/fmt/format.h:3351:9: note: non-constexpr function 'ceil' cannot be used in a constant expression
        std::ceil((f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10));

std::ceil is constexpr since C++23 (https://en.cppreference.com/w/cpp/numeric/math/ceil)

This is not a compiler bug. This is unimplemented feature in clang and msvs. Only gcc support it.
https://en.cppreference.com/w/cpp/compiler_support

This comment has been minimized.

Copy link
@vitaut

vitaut May 23, 2023

Author Contributor

Yeah, we need to gate the tests on ceil constexpr'ness.

This comment has been minimized.

Copy link
@phprus

phprus May 23, 2023

Contributor

I have one idea...

This comment has been minimized.

Copy link
@phprus

phprus May 23, 2023

Contributor
dragon_flags = dragon::fixup;
} else if (!is_constant_evaluated() && precision < 0) {
} else if (precision < 0) {
// Use Dragonbox for the shortest format.
if (specs.binary32) {
auto dec = dragonbox::to_decimal(static_cast<float>(value));
Expand All @@ -3526,25 +3360,6 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs,
auto dec = dragonbox::to_decimal(static_cast<double>(value));
write<char>(buffer_appender<char>(buf), dec.significand);
return dec.exponent;
} else if (is_constant_evaluated()) {
// Use Grisu + Dragon4 for the given precision:
// https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf.
const int min_exp = -60; // alpha in Grisu.
int cached_exp10 = 0; // K in Grisu.
fp normalized = normalize(fp(converted_value));
const auto cached_pow = get_cached_power(
min_exp - (normalized.e + fp::num_significand_bits), cached_exp10);
normalized = normalized * cached_pow;
gen_digits_handler handler{buf.data(), 0, precision, -cached_exp10, fixed};
if (grisu_gen_digits(normalized, 1, exp, handler) != digits::error &&
!is_constant_evaluated()) {
exp += handler.exp10;
buf.try_resize(to_unsigned(handler.size));
use_dragon = false;
} else {
exp += handler.size - cached_exp10 - 1;
precision = handler.precision;
}
} else {
// Extract significand bits and exponent bits.
using info = dragonbox::float_info<double>;
Expand Down
3 changes: 2 additions & 1 deletion test/compile-fp-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

#if defined(__cpp_lib_bit_cast) && __cpp_lib_bit_cast >= 201806 && \
defined(__cpp_constexpr) && __cpp_constexpr >= 201907 && \
defined(__cpp_constexpr_dynamic_alloc) && \
defined(__cpp_constexpr_dynamic_alloc) && !FMT_MSC_VERSION && \
__cpp_constexpr_dynamic_alloc >= 201907 && FMT_CPLUSPLUS >= 202002L

template <size_t max_string_length, typename Char = char> struct test_string {
template <typename T> constexpr bool operator==(const T& rhs) const noexcept {
return fmt::basic_string_view<Char>(rhs).compare(buffer) == 0;
Expand Down
10 changes: 4 additions & 6 deletions test/compile-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,12 @@ TEST(compile_test, format_to_n) {
EXPECT_STREQ("2a", buffer);
}

#ifdef __cpp_lib_bit_cast
# ifdef __cpp_lib_bit_cast
TEST(compile_test, constexpr_formatted_size) {
FMT_CONSTEXPR20 size_t s1 = fmt::formatted_size(FMT_COMPILE("{0}"), 42);
EXPECT_EQ(2, s1);
FMT_CONSTEXPR20 size_t s2 = fmt::formatted_size(FMT_COMPILE("{0:<4.2f}"), 42.0);
EXPECT_EQ(5, s2);
FMT_CONSTEXPR20 size_t size = fmt::formatted_size(FMT_COMPILE("{}"), 42);
EXPECT_EQ(size, 2);
}
#endif
# endif

TEST(compile_test, text_and_arg) {
EXPECT_EQ(">>>42<<<", fmt::format(FMT_COMPILE(">>>{}<<<"), 42));
Expand Down
43 changes: 0 additions & 43 deletions test/format-impl-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -249,49 +249,6 @@ TEST(fp_test, dragonbox_max_k) {
2 * fmt::detail::num_significand_bits<double>() - 1));
}

TEST(fp_test, get_round_direction) {
using fmt::detail::get_round_direction;
using fmt::detail::round_direction;
EXPECT_EQ(get_round_direction(100, 50, 0), round_direction::down);
EXPECT_EQ(get_round_direction(100, 51, 0), round_direction::up);
EXPECT_EQ(get_round_direction(100, 40, 10), round_direction::down);
EXPECT_EQ(get_round_direction(100, 60, 10), round_direction::up);
for (size_t i = 41; i < 60; ++i)
EXPECT_EQ(get_round_direction(100, i, 10), round_direction::unknown);
uint64_t max = max_value<uint64_t>();
EXPECT_THROW(get_round_direction(100, 100, 0), assertion_failure);
EXPECT_THROW(get_round_direction(100, 0, 100), assertion_failure);
EXPECT_THROW(get_round_direction(100, 0, 50), assertion_failure);
// Check that remainder + error doesn't overflow.
EXPECT_EQ(get_round_direction(max, max - 1, 2), round_direction::up);
// Check that 2 * (remainder + error) doesn't overflow.
EXPECT_EQ(get_round_direction(max, max / 2 + 1, max / 2),
round_direction::unknown);
// Check that remainder - error doesn't overflow.
EXPECT_EQ(get_round_direction(100, 40, 41), round_direction::unknown);
// Check that 2 * (remainder - error) doesn't overflow.
EXPECT_EQ(get_round_direction(max, max - 1, 1), round_direction::up);
}

TEST(fp_test, fixed_handler) {
struct handler : fmt::detail::gen_digits_handler {
char buffer[10];
handler(int prec = 0) : fmt::detail::gen_digits_handler() {
buf = buffer;
precision = prec;
}
};
handler().on_digit('0', 100, 99, 0, false);
EXPECT_THROW(handler().on_digit('0', 100, 100, 0, false), assertion_failure);
namespace digits = fmt::detail::digits;
EXPECT_EQ(handler(1).on_digit('0', 100, 10, 10, false), digits::error);
// Check that divisor - error doesn't overflow.
EXPECT_EQ(handler(1).on_digit('0', 100, 10, 101, false), digits::error);
// Check that 2 * error doesn't overflow.
uint64_t max = max_value<uint64_t>();
EXPECT_EQ(handler(1).on_digit('0', max, 10, max - 1, false), digits::error);
}

TEST(fp_test, grisu_format_compiles_with_on_ieee_double) {
auto buf = fmt::memory_buffer();
format_float(0.42, -1, fmt::detail::float_specs(), buf);
Expand Down

0 comments on commit 858e528

Please sign in to comment.