libstdc++: Shortest denormal hex std::to_chars
Checks
Commit Message
On Fri, Oct 28, 2022 at 12:52:44PM -0400, Patrick Palka wrote:
> > The following patch on top of
> > https://gcc.gnu.org/pipermail/libstdc++/2022-October/054849.html
> > adds std::{,b}float16_t support for std::to_chars.
> > When precision is specified (or for std::bfloat16_t for hex mode even if not),
> > I believe we can just use the std::to_chars float (when float is mode
> > compatible with std::float32_t) overloads, both formats are proper subsets
> > of std::float32_t.
> > Unfortunately when precision is not specified and we are supposed to emit
> > shortest string, the std::{,b}float16_t strings are usually much shorter.
> > E.g. 1.e7p-14f16 shortest fixed representation is
> > 0.0001161 and shortest scientific representation is
> > 1.161e-04 while 1.e7p-14f32 (same number promoted to std::float32_t)
> > 0.00011610985 and
> > 1.1610985e-04.
> > Similarly for 1.38p-112bf16,
> > 0.000000000000000000000000000000000235
> > 2.35e-34 vs. 1.38p-112f32
> > 0.00000000000000000000000000000000023472271
> > 2.3472271e-34
> > For std::float16_t there are differences even in the shortest hex, say:
> > 0.01p-14 vs. 1p-22
> > but only for denormal std::float16_t values (where all std::float16_t
> > denormals converted to std::float32_t are normal), __FLT16_MIN__ and
> > everything larger in absolute value than that is the same. Unless
> > that is a bug and we should try to discover shorter representations
> > even for denormals...
>
> IIRC for hex formatting of denormals I opted to be consistent with how
> glibc printf formats them, instead of outputting the truly shortest
> form.
>
> I wouldn't be against using the float32 overloads even for shortest hex
> formatting of float16. The output is shorter but equivalent so it
> shouldn't cause any problems.
The following patch changes the behavior of the shortest hex denormals,
such that they are printed like normals (so for has_implicit_leading_bit
with 1p-149 instead of 0.000002p-126 etc., otherwise (Intel extended)
with the leading digit before dot being [89abcdef]). I think for all the
supported format it is never longer, it can be equal length e.g. for
0.fffffep-126 vs. 1.fffffcp-127 but fortunately no largest subnormal
in any format has the unbiased exponent like -9, -99, -999, -9999 because
then it would be longer and often it is shorter, sometimes much shorter.
For the cases with precision it keeps the handling as is.
While for !has_implicit_leading_bit we for normals or with this patch
even denormals have really shortest representation, for other formats
we sometimes do not, but this patch doesn't deal with that (we
always use 1.NNN while we could use 1.NNN up to f.NNN and by that shortening
by the last hexit if the last hexit doesn't have least significant bit set
and unbiased exponent is not -9, -99, -999 or -9999.
Tested on x86_64-linux (on top of the 3 to/from_chars {,b}float16_t
patches).
2022-11-01 Jakub Jelinek <jakub@redhat.com>
* src/c++17/floating_to_chars.cc (__floating_to_chars_hex): Drop const
from unbiased_exponent. Canonicalize denormals such that they have
the leading bit set by shifting effective mantissa up and decreasing
unbiased_exponent.
(__floating_to_chars_shortest): Don't instantiate
__floating_to_chars_hex for float16_t either and use float instead.
* testsuite/20_util/to_chars/float.cc (float_to_chars_test_cases):
Adjust testcases for shortest hex denormals.
* testsuite/20_util/to_chars/double.cc (double_to_chars_test_cases):
Likewise.
Jakub
Comments
On Tue, 1 Nov 2022 at 12:18, Jakub Jelinek <jakub@redhat.com> wrote:
>
> On Fri, Oct 28, 2022 at 12:52:44PM -0400, Patrick Palka wrote:
> > > The following patch on top of
> > > https://gcc.gnu.org/pipermail/libstdc++/2022-October/054849.html
> > > adds std::{,b}float16_t support for std::to_chars.
> > > When precision is specified (or for std::bfloat16_t for hex mode even if not),
> > > I believe we can just use the std::to_chars float (when float is mode
> > > compatible with std::float32_t) overloads, both formats are proper subsets
> > > of std::float32_t.
> > > Unfortunately when precision is not specified and we are supposed to emit
> > > shortest string, the std::{,b}float16_t strings are usually much shorter.
> > > E.g. 1.e7p-14f16 shortest fixed representation is
> > > 0.0001161 and shortest scientific representation is
> > > 1.161e-04 while 1.e7p-14f32 (same number promoted to std::float32_t)
> > > 0.00011610985 and
> > > 1.1610985e-04.
> > > Similarly for 1.38p-112bf16,
> > > 0.000000000000000000000000000000000235
> > > 2.35e-34 vs. 1.38p-112f32
> > > 0.00000000000000000000000000000000023472271
> > > 2.3472271e-34
> > > For std::float16_t there are differences even in the shortest hex, say:
> > > 0.01p-14 vs. 1p-22
> > > but only for denormal std::float16_t values (where all std::float16_t
> > > denormals converted to std::float32_t are normal), __FLT16_MIN__ and
> > > everything larger in absolute value than that is the same. Unless
> > > that is a bug and we should try to discover shorter representations
> > > even for denormals...
> >
> > IIRC for hex formatting of denormals I opted to be consistent with how
> > glibc printf formats them, instead of outputting the truly shortest
> > form.
> >
> > I wouldn't be against using the float32 overloads even for shortest hex
> > formatting of float16. The output is shorter but equivalent so it
> > shouldn't cause any problems.
>
> The following patch changes the behavior of the shortest hex denormals,
> such that they are printed like normals (so for has_implicit_leading_bit
> with 1p-149 instead of 0.000002p-126 etc., otherwise (Intel extended)
> with the leading digit before dot being [89abcdef]). I think for all the
> supported format it is never longer, it can be equal length e.g. for
> 0.fffffep-126 vs. 1.fffffcp-127 but fortunately no largest subnormal
> in any format has the unbiased exponent like -9, -99, -999, -9999 because
> then it would be longer and often it is shorter, sometimes much shorter.
>
> For the cases with precision it keeps the handling as is.
>
> While for !has_implicit_leading_bit we for normals or with this patch
> even denormals have really shortest representation, for other formats
> we sometimes do not, but this patch doesn't deal with that (we
> always use 1.NNN while we could use 1.NNN up to f.NNN and by that shortening
> by the last hexit if the last hexit doesn't have least significant bit set
> and unbiased exponent is not -9, -99, -999 or -9999.
>
> Tested on x86_64-linux (on top of the 3 to/from_chars {,b}float16_t
> patches).
This looks good to me. Please give Patrick a chance to comment, but
it's approved for trunk unless he objects. Thanks!
>
> 2022-11-01 Jakub Jelinek <jakub@redhat.com>
>
> * src/c++17/floating_to_chars.cc (__floating_to_chars_hex): Drop const
> from unbiased_exponent. Canonicalize denormals such that they have
> the leading bit set by shifting effective mantissa up and decreasing
> unbiased_exponent.
> (__floating_to_chars_shortest): Don't instantiate
> __floating_to_chars_hex for float16_t either and use float instead.
> * testsuite/20_util/to_chars/float.cc (float_to_chars_test_cases):
> Adjust testcases for shortest hex denormals.
> * testsuite/20_util/to_chars/double.cc (double_to_chars_test_cases):
> Likewise.
>
> --- libstdc++-v3/src/c++17/floating_to_chars.cc.jj 2022-10-31 22:20:35.881121902 +0100
> +++ libstdc++-v3/src/c++17/floating_to_chars.cc 2022-11-01 12:16:14.352652455 +0100
> @@ -844,9 +844,9 @@ template<typename T>
> const bool is_normal_number = (biased_exponent != 0);
>
> // Calculate the unbiased exponent.
> - const int32_t unbiased_exponent = (is_normal_number
> - ? biased_exponent - exponent_bias
> - : 1 - exponent_bias);
> + int32_t unbiased_exponent = (is_normal_number
> + ? biased_exponent - exponent_bias
> + : 1 - exponent_bias);
>
> // Shift the mantissa so that its bitwidth is a multiple of 4.
> constexpr unsigned rounded_mantissa_bits = (mantissa_bits + 3) / 4 * 4;
> @@ -863,6 +863,16 @@ template<typename T>
> __glibcxx_assert(effective_mantissa & (mantissa_t{1} << (mantissa_bits
> - 1u)));
> }
> + else if (!precision.has_value() && effective_mantissa)
> + {
> + // 1.8p-23 is shorter than 0.00cp-14, so if precision is
> + // omitted, try to canonicalize denormals such that they
> + // have the leading bit set.
> + int width = __bit_width(effective_mantissa);
> + int shift = rounded_mantissa_bits - width + has_implicit_leading_bit;
> + unbiased_exponent -= shift;
> + effective_mantissa <<= shift;
> + }
>
> // Compute the shortest precision needed to print this value exactly,
> // disregarding trailing zeros.
> @@ -1061,7 +1071,10 @@ template<typename T>
> // std::bfloat16_t has the same exponent range as std::float32_t
> // and so we can avoid instantiation of __floating_to_chars_hex
> // for bfloat16_t. Shortest hex will be the same as for float.
> - if constexpr (is_same_v<T, floating_type_bfloat16_t>)
> + // When we print shortest form even for denormals, we can do it
> + // for std::float16_t as well.
> + if constexpr (is_same_v<T, floating_type_float16_t>
> + || is_same_v<T, floating_type_bfloat16_t>)
> return __floating_to_chars_hex(first, last, value.x, nullopt);
> else
> return __floating_to_chars_hex(first, last, value, nullopt);
> --- libstdc++-v3/testsuite/20_util/to_chars/float.cc.jj 2022-01-11 22:31:41.605755528 +0100
> +++ libstdc++-v3/testsuite/20_util/to_chars/float.cc 2022-11-01 12:34:21.370882443 +0100
> @@ -521,8 +521,8 @@ inline constexpr float_to_chars_testcase
>
> // Test hexfloat corner cases.
> {0x1.728p+0f, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
> - {0x0.000002p-126f, chars_format::hex, "0.000002p-126"}, // instead of "1p-149", min subnormal
> - {0x0.fffffep-126f, chars_format::hex, "0.fffffep-126"}, // max subnormal
> + {0x0.000002p-126f, chars_format::hex, "1p-149"}, // min subnormal
> + {0x0.fffffep-126f, chars_format::hex, "1.fffffcp-127"}, // max subnormal
> {0x1p-126f, chars_format::hex, "1p-126"}, // min normal
> {0x1.fffffep+127f, chars_format::hex, "1.fffffep+127"}, // max normal
>
> --- libstdc++-v3/testsuite/20_util/to_chars/double.cc.jj 2022-01-11 22:31:41.604755542 +0100
> +++ libstdc++-v3/testsuite/20_util/to_chars/double.cc 2022-11-01 12:42:39.753112522 +0100
> @@ -2821,8 +2821,8 @@ inline constexpr double_to_chars_testcas
>
> // Test hexfloat corner cases.
> {0x1.728p+0, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
> - {0x0.0000000000001p-1022, chars_format::hex, "0.0000000000001p-1022"}, // instead of "1p-1074", min subnormal
> - {0x0.fffffffffffffp-1022, chars_format::hex, "0.fffffffffffffp-1022"}, // max subnormal
> + {0x0.0000000000001p-1022, chars_format::hex, "1p-1074"}, // min subnormal
> + {0x0.fffffffffffffp-1022, chars_format::hex, "1.ffffffffffffep-1023"}, // max subnormal
> {0x1p-1022, chars_format::hex, "1p-1022"}, // min normal
> {0x1.fffffffffffffp+1023, chars_format::hex, "1.fffffffffffffp+1023"}, // max normal
>
>
> Jakub
>
On Tue, 1 Nov 2022, Jonathan Wakely wrote:
> On Tue, 1 Nov 2022 at 12:18, Jakub Jelinek <jakub@redhat.com> wrote:
> >
> > On Fri, Oct 28, 2022 at 12:52:44PM -0400, Patrick Palka wrote:
> > > > The following patch on top of
> > > > https://gcc.gnu.org/pipermail/libstdc++/2022-October/054849.html
> > > > adds std::{,b}float16_t support for std::to_chars.
> > > > When precision is specified (or for std::bfloat16_t for hex mode even if not),
> > > > I believe we can just use the std::to_chars float (when float is mode
> > > > compatible with std::float32_t) overloads, both formats are proper subsets
> > > > of std::float32_t.
> > > > Unfortunately when precision is not specified and we are supposed to emit
> > > > shortest string, the std::{,b}float16_t strings are usually much shorter.
> > > > E.g. 1.e7p-14f16 shortest fixed representation is
> > > > 0.0001161 and shortest scientific representation is
> > > > 1.161e-04 while 1.e7p-14f32 (same number promoted to std::float32_t)
> > > > 0.00011610985 and
> > > > 1.1610985e-04.
> > > > Similarly for 1.38p-112bf16,
> > > > 0.000000000000000000000000000000000235
> > > > 2.35e-34 vs. 1.38p-112f32
> > > > 0.00000000000000000000000000000000023472271
> > > > 2.3472271e-34
> > > > For std::float16_t there are differences even in the shortest hex, say:
> > > > 0.01p-14 vs. 1p-22
> > > > but only for denormal std::float16_t values (where all std::float16_t
> > > > denormals converted to std::float32_t are normal), __FLT16_MIN__ and
> > > > everything larger in absolute value than that is the same. Unless
> > > > that is a bug and we should try to discover shorter representations
> > > > even for denormals...
> > >
> > > IIRC for hex formatting of denormals I opted to be consistent with how
> > > glibc printf formats them, instead of outputting the truly shortest
> > > form.
> > >
> > > I wouldn't be against using the float32 overloads even for shortest hex
> > > formatting of float16. The output is shorter but equivalent so it
> > > shouldn't cause any problems.
> >
> > The following patch changes the behavior of the shortest hex denormals,
> > such that they are printed like normals (so for has_implicit_leading_bit
> > with 1p-149 instead of 0.000002p-126 etc., otherwise (Intel extended)
> > with the leading digit before dot being [89abcdef]). I think for all the
> > supported format it is never longer, it can be equal length e.g. for
> > 0.fffffep-126 vs. 1.fffffcp-127 but fortunately no largest subnormal
> > in any format has the unbiased exponent like -9, -99, -999, -9999 because
> > then it would be longer and often it is shorter, sometimes much shorter.
> >
> > For the cases with precision it keeps the handling as is.
> >
> > While for !has_implicit_leading_bit we for normals or with this patch
> > even denormals have really shortest representation, for other formats
> > we sometimes do not, but this patch doesn't deal with that (we
> > always use 1.NNN while we could use 1.NNN up to f.NNN and by that shortening
> > by the last hexit if the last hexit doesn't have least significant bit set
> > and unbiased exponent is not -9, -99, -999 or -9999.
> >
> > Tested on x86_64-linux (on top of the 3 to/from_chars {,b}float16_t
> > patches).
>
> This looks good to me. Please give Patrick a chance to comment, but
> it's approved for trunk unless he objects. Thanks!
LGTM. This'll mean the output of to_chars(denormal, hex, precision)
will no longer be based on the shortest form to_chars(denormal, hex)
which slightly bothers me, but doesn't seem to be nonconforming either.
>
>
> >
> > 2022-11-01 Jakub Jelinek <jakub@redhat.com>
> >
> > * src/c++17/floating_to_chars.cc (__floating_to_chars_hex): Drop const
> > from unbiased_exponent. Canonicalize denormals such that they have
> > the leading bit set by shifting effective mantissa up and decreasing
> > unbiased_exponent.
> > (__floating_to_chars_shortest): Don't instantiate
> > __floating_to_chars_hex for float16_t either and use float instead.
> > * testsuite/20_util/to_chars/float.cc (float_to_chars_test_cases):
> > Adjust testcases for shortest hex denormals.
> > * testsuite/20_util/to_chars/double.cc (double_to_chars_test_cases):
> > Likewise.
> >
> > --- libstdc++-v3/src/c++17/floating_to_chars.cc.jj 2022-10-31 22:20:35.881121902 +0100
> > +++ libstdc++-v3/src/c++17/floating_to_chars.cc 2022-11-01 12:16:14.352652455 +0100
> > @@ -844,9 +844,9 @@ template<typename T>
> > const bool is_normal_number = (biased_exponent != 0);
> >
> > // Calculate the unbiased exponent.
> > - const int32_t unbiased_exponent = (is_normal_number
> > - ? biased_exponent - exponent_bias
> > - : 1 - exponent_bias);
> > + int32_t unbiased_exponent = (is_normal_number
> > + ? biased_exponent - exponent_bias
> > + : 1 - exponent_bias);
> >
> > // Shift the mantissa so that its bitwidth is a multiple of 4.
> > constexpr unsigned rounded_mantissa_bits = (mantissa_bits + 3) / 4 * 4;
> > @@ -863,6 +863,16 @@ template<typename T>
> > __glibcxx_assert(effective_mantissa & (mantissa_t{1} << (mantissa_bits
> > - 1u)));
> > }
> > + else if (!precision.has_value() && effective_mantissa)
> > + {
> > + // 1.8p-23 is shorter than 0.00cp-14, so if precision is
> > + // omitted, try to canonicalize denormals such that they
> > + // have the leading bit set.
> > + int width = __bit_width(effective_mantissa);
> > + int shift = rounded_mantissa_bits - width + has_implicit_leading_bit;
> > + unbiased_exponent -= shift;
> > + effective_mantissa <<= shift;
> > + }
> >
> > // Compute the shortest precision needed to print this value exactly,
> > // disregarding trailing zeros.
> > @@ -1061,7 +1071,10 @@ template<typename T>
> > // std::bfloat16_t has the same exponent range as std::float32_t
> > // and so we can avoid instantiation of __floating_to_chars_hex
> > // for bfloat16_t. Shortest hex will be the same as for float.
> > - if constexpr (is_same_v<T, floating_type_bfloat16_t>)
> > + // When we print shortest form even for denormals, we can do it
> > + // for std::float16_t as well.
> > + if constexpr (is_same_v<T, floating_type_float16_t>
> > + || is_same_v<T, floating_type_bfloat16_t>)
> > return __floating_to_chars_hex(first, last, value.x, nullopt);
> > else
> > return __floating_to_chars_hex(first, last, value, nullopt);
> > --- libstdc++-v3/testsuite/20_util/to_chars/float.cc.jj 2022-01-11 22:31:41.605755528 +0100
> > +++ libstdc++-v3/testsuite/20_util/to_chars/float.cc 2022-11-01 12:34:21.370882443 +0100
> > @@ -521,8 +521,8 @@ inline constexpr float_to_chars_testcase
> >
> > // Test hexfloat corner cases.
> > {0x1.728p+0f, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
> > - {0x0.000002p-126f, chars_format::hex, "0.000002p-126"}, // instead of "1p-149", min subnormal
> > - {0x0.fffffep-126f, chars_format::hex, "0.fffffep-126"}, // max subnormal
> > + {0x0.000002p-126f, chars_format::hex, "1p-149"}, // min subnormal
> > + {0x0.fffffep-126f, chars_format::hex, "1.fffffcp-127"}, // max subnormal
> > {0x1p-126f, chars_format::hex, "1p-126"}, // min normal
> > {0x1.fffffep+127f, chars_format::hex, "1.fffffep+127"}, // max normal
> >
> > --- libstdc++-v3/testsuite/20_util/to_chars/double.cc.jj 2022-01-11 22:31:41.604755542 +0100
> > +++ libstdc++-v3/testsuite/20_util/to_chars/double.cc 2022-11-01 12:42:39.753112522 +0100
> > @@ -2821,8 +2821,8 @@ inline constexpr double_to_chars_testcas
> >
> > // Test hexfloat corner cases.
> > {0x1.728p+0, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
> > - {0x0.0000000000001p-1022, chars_format::hex, "0.0000000000001p-1022"}, // instead of "1p-1074", min subnormal
> > - {0x0.fffffffffffffp-1022, chars_format::hex, "0.fffffffffffffp-1022"}, // max subnormal
> > + {0x0.0000000000001p-1022, chars_format::hex, "1p-1074"}, // min subnormal
> > + {0x0.fffffffffffffp-1022, chars_format::hex, "1.ffffffffffffep-1023"}, // max subnormal
> > {0x1p-1022, chars_format::hex, "1p-1022"}, // min normal
> > {0x1.fffffffffffffp+1023, chars_format::hex, "1.fffffffffffffp+1023"}, // max normal
> >
> >
> > Jakub
> >
>
>
@@ -844,9 +844,9 @@ template<typename T>
const bool is_normal_number = (biased_exponent != 0);
// Calculate the unbiased exponent.
- const int32_t unbiased_exponent = (is_normal_number
- ? biased_exponent - exponent_bias
- : 1 - exponent_bias);
+ int32_t unbiased_exponent = (is_normal_number
+ ? biased_exponent - exponent_bias
+ : 1 - exponent_bias);
// Shift the mantissa so that its bitwidth is a multiple of 4.
constexpr unsigned rounded_mantissa_bits = (mantissa_bits + 3) / 4 * 4;
@@ -863,6 +863,16 @@ template<typename T>
__glibcxx_assert(effective_mantissa & (mantissa_t{1} << (mantissa_bits
- 1u)));
}
+ else if (!precision.has_value() && effective_mantissa)
+ {
+ // 1.8p-23 is shorter than 0.00cp-14, so if precision is
+ // omitted, try to canonicalize denormals such that they
+ // have the leading bit set.
+ int width = __bit_width(effective_mantissa);
+ int shift = rounded_mantissa_bits - width + has_implicit_leading_bit;
+ unbiased_exponent -= shift;
+ effective_mantissa <<= shift;
+ }
// Compute the shortest precision needed to print this value exactly,
// disregarding trailing zeros.
@@ -1061,7 +1071,10 @@ template<typename T>
// std::bfloat16_t has the same exponent range as std::float32_t
// and so we can avoid instantiation of __floating_to_chars_hex
// for bfloat16_t. Shortest hex will be the same as for float.
- if constexpr (is_same_v<T, floating_type_bfloat16_t>)
+ // When we print shortest form even for denormals, we can do it
+ // for std::float16_t as well.
+ if constexpr (is_same_v<T, floating_type_float16_t>
+ || is_same_v<T, floating_type_bfloat16_t>)
return __floating_to_chars_hex(first, last, value.x, nullopt);
else
return __floating_to_chars_hex(first, last, value, nullopt);
@@ -521,8 +521,8 @@ inline constexpr float_to_chars_testcase
// Test hexfloat corner cases.
{0x1.728p+0f, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
- {0x0.000002p-126f, chars_format::hex, "0.000002p-126"}, // instead of "1p-149", min subnormal
- {0x0.fffffep-126f, chars_format::hex, "0.fffffep-126"}, // max subnormal
+ {0x0.000002p-126f, chars_format::hex, "1p-149"}, // min subnormal
+ {0x0.fffffep-126f, chars_format::hex, "1.fffffcp-127"}, // max subnormal
{0x1p-126f, chars_format::hex, "1p-126"}, // min normal
{0x1.fffffep+127f, chars_format::hex, "1.fffffep+127"}, // max normal
@@ -2821,8 +2821,8 @@ inline constexpr double_to_chars_testcas
// Test hexfloat corner cases.
{0x1.728p+0, chars_format::hex, "1.728p+0"}, // instead of "2.e5p-1"
- {0x0.0000000000001p-1022, chars_format::hex, "0.0000000000001p-1022"}, // instead of "1p-1074", min subnormal
- {0x0.fffffffffffffp-1022, chars_format::hex, "0.fffffffffffffp-1022"}, // max subnormal
+ {0x0.0000000000001p-1022, chars_format::hex, "1p-1074"}, // min subnormal
+ {0x0.fffffffffffffp-1022, chars_format::hex, "1.ffffffffffffep-1023"}, // max subnormal
{0x1p-1022, chars_format::hex, "1p-1022"}, // min normal
{0x1.fffffffffffffp+1023, chars_format::hex, "1.fffffffffffffp+1023"}, // max normal