Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support single precision floats in grisu formatting #1361

Merged
merged 1 commit into from
Oct 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion include/fmt/format-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,18 @@ class fp {
lower.f <<= lower.e - upper.e;
lower.e = upper.e;
}

void compute_float_boundaries(fp& lower, fp& upper) const {
constexpr int min_normal_e = std::numeric_limits<float>::min_exponent -
std::numeric_limits<double>::digits;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The smallest positive normal float is 2^-126. fp(2^-126) is fp(2^52, -178). float min_exponent is -125, double digits is 53: both of these numbers are off by one from what we need, but these 1s cancel each other out.

significand_type half_ulp = 1 << (std::numeric_limits<double>::digits -
std::numeric_limits<float>::digits - 1);
if (min_normal_e > e) half_ulp <<= min_normal_e - e;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the float is normal, half_ulp is an obvious constant. When the float is subnormal, the cast to double shifts the significand to the left and effectively decreases the exponent by the number of shifted bits. Then we need to shift half_ulp to the left by the same amount.

upper = normalize<0>(fp(f + half_ulp, e));
lower = fp(f - (half_ulp >> (f == implicit_bit && e > min_normal_e)), e);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition is e > min_normal_e, not e >= min_normal_e, because the smallest positive normal float is the average of its neighbors. (The same is true for the smallest positive normal double but compute_boundaries does not account for that which may be either a bug or irrelevant.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same is true for the smallest positive normal double but compute_boundaries does not account for that which may be either a bug or irrelevant.

Great catch! It's definitely a bug which fortunately doesn't affect the output. Will be fixed in eb879a6.

lower.f <<= lower.e - upper.e;
lower.e = upper.e;
}
};

// Returns an fp number representing x - y. Result may not be normalized.
Expand Down Expand Up @@ -1045,7 +1057,11 @@ bool grisu_format(Double value, buffer<char>& buf, int precision,
buf.resize(to_unsigned(handler.size));
} else {
fp lower, upper; // w^- and w^+ in the Grisu paper.
fp_value.compute_boundaries(lower, upper);
if ((options & grisu_options::binary32) != 0)
fp_value.compute_float_boundaries(lower, upper);
else
fp_value.compute_boundaries(lower, upper);

// Find a cached power of 10 such that multiplying upper by it will bring
// the exponent in the range [min_exp, -32].
const auto cached_pow = get_cached_power( // \tilde{c}_{-k} in Grisu.
Expand Down
8 changes: 6 additions & 2 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ It grisu_prettify(const char* digits, int size, int exp, It it,
}

namespace grisu_options {
enum { fixed = 1, grisu2 = 2 };
enum { fixed = 1, grisu2 = 2, binary32 = 4 };
}

// Formats value using the Grisu algorithm:
Expand Down Expand Up @@ -2809,12 +2809,16 @@ void internal::basic_writer<Range>::write_fp(T value,
memory_buffer buffer;
int exp = 0;
int precision = specs.precision >= 0 || !specs.type ? specs.precision : 6;
unsigned options = 0;
if (handler.fixed) options |= internal::grisu_options::fixed;
if (sizeof(value) == sizeof(float))
options |= internal::grisu_options::binary32;
bool use_grisu = USE_GRISU &&
(specs.type != 'a' && specs.type != 'A' &&
specs.type != 'e' && specs.type != 'E') &&
internal::grisu_format(
static_cast<double>(value), buffer, precision,
handler.fixed ? internal::grisu_options::fixed : 0, exp);
options, exp);
char* decimal_point_pos = nullptr;
if (!use_grisu)
decimal_point_pos = internal::sprintf_format(value, buffer, specs);
Expand Down
30 changes: 30 additions & 0 deletions test/format-impl-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,36 @@ TEST(FPTest, ComputeBoundaries) {
EXPECT_EQ(31, upper.e);
}

TEST(FPTest, ComputeFloatBoundaries) {
struct {
double x, lower, upper;
} tests[] = {
// regular
{1.5f, 1.4999999403953552, 1.5000000596046448},
// boundary
{1.0f, 0.9999999701976776, 1.0000000596046448},
// min normal
{1.1754944e-38f, 1.1754942807573643e-38, 1.1754944208872107e-38},
// max subnormal
{1.1754942e-38f, 1.1754941406275179e-38, 1.1754942807573643e-38},
// min subnormal
{1e-45f, 7.006492321624085e-46, 2.1019476964872256e-45},
};
for (auto test : tests) {
auto v = fp(test.x);
fp vlower = normalize(fp(test.lower));
fp vupper = normalize(fp(test.upper));
vlower.f >>= vupper.e - vlower.e;
vlower.e = vupper.e;
fp lower, upper;
v.compute_float_boundaries(lower, upper);
EXPECT_EQ(vlower.f, lower.f);
EXPECT_EQ(vlower.e, lower.e);
EXPECT_EQ(vupper.f, upper.f);
EXPECT_EQ(vupper.e, upper.e);
}
}

TEST(FPTest, Subtract) {
auto v = fp(123, 1) - fp(102, 1);
EXPECT_EQ(v.f, 21u);
Expand Down
2 changes: 2 additions & 0 deletions test/grisu-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ TEST(GrisuTest, Prettify) {
EXPECT_EQ("12340000000.0", fmt::format("{}", 1234e7));
EXPECT_EQ("12.34", fmt::format("{}", 1234e-2));
EXPECT_EQ("0.001234", fmt::format("{}", 1234e-6));
EXPECT_EQ("0.1", fmt::format("{}", 0.1f));
EXPECT_EQ("0.10000000149011612", fmt::format("{}", double(0.1f)));
}

TEST(GrisuTest, ZeroPrecision) { EXPECT_EQ("1", fmt::format("{:.0}", 1.0)); }
Expand Down