On Thu, 27 Oct 2022, Jakub Jelinek wrote:

> Hi!
> 
> 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.

> std::bfloat16_t has the same exponent range as std::float32_t, so all
> std::bfloat16_t denormals are also std::float32_t denormals and thus
> the shortest hex representations are the same.
> 
> As documented, ryu can handle arbitrary IEEE like floating point formats
> (probably not wider than IEEE quad) using the generic_128 handling, but
> ryu is hidden in libstdc++.so.  As only few architectures support
> std::float16_t right now and some of them have special ISA requirements
> for those (e.g. on i?86 one needs -msse2) and std::bfloat16_t is right
> now supported only on x86 (again with -msse2), perhaps with aarch64/arm
> coming next if ARM is interested, but I think it is possible that more
> will be added later, instead of exporting APIs from the library to handle
> directly the std::{,b}float16_t overloads this patch instead exports
> functions which take a float which is a superset of those and expects
> the inline overloads to promote the 16-bit formats to 32-bit, then inside
> of the library it ensures they are printed right.
> With the added [[gnu::cold]] attribute because I think most users
> will primarily use these formats as storage formats and perform arithmetics
> in the excess precision for them and print also as std::float32_t the
> added support doesn't seem to be too large, on x86_64:
> readelf -Ws libstdc++.so.6.0.31 | grep float16_t
>    912: 00000000000ae824   950 FUNC    GLOBAL DEFAULT   13 
> _ZSt21__to_chars_bfloat16_tPcS_fSt12chars_format@@GLIBCXX_3.4.31
>   5767: 00000000000ae4a1   899 FUNC    GLOBAL DEFAULT   13 
> _ZSt20__to_chars_float16_tPcS_fSt12chars_format@@GLIBCXX_3.4.31
>    842: 000000000016d430   106 FUNC    LOCAL  DEFAULT   13 
> _ZN12_GLOBAL__N_113get_ieee_reprINS_23floating_type_float16_tEEENS_6ieee_tIT_EES3_
>    865: 0000000000170980  1613 FUNC    LOCAL  DEFAULT   13 
> _ZSt23__floating_to_chars_hexIN12_GLOBAL__N_123floating_type_float16_tEESt15to_chars_resultPcS3_T_St8optionalIiE.constprop.0.isra.0
>   7205: 00000000000ae824   950 FUNC    GLOBAL DEFAULT   13 
> _ZSt21__to_chars_bfloat16_tPcS_fSt12chars_format
>   7985: 00000000000ae4a1   899 FUNC    GLOBAL DEFAULT   13 
> _ZSt20__to_chars_float16_tPcS_fSt12chars_format
> so 3568 code bytes together or so.

Ouch, the instantiation of __floating_to_chars_hex for float16 is
responsible for nearly 50% of the .so size increase

> 
> Tested with the attached test (which doesn't prove the shortest
> representation, just prints std::{,b}float16_t and std::float32_t
> shortest strings side by side, then tries to verify it can be
> emitted even into the exact sized range and can't be into range
> one smaller than that and tries to read what is printed
> back using from_chars float32_t overload (so there could be
> double rounding, but apparently there is none for the shortest strings).
> The only differences printed are for NaNs, where sNaNs are canonicalized
> to canonical qNaNs and as to_chars doesn't print NaN mantissa, even qNaNs
> other than the canonical one are read back just as the canonical NaN.
> 
> Also attaching what Patrick wrote to generate the pow10_adjustment_tab,
> for std::float16_t only 1.0, 10.0, 100.0, 1000.0 and 10000.0 are powers
> of 10 in the range because __FLT16_MAX__ is 65504.0, and all of the above
> are exactly representable in std::float16_t, so we want to use 0 in
> pow10_adjustment_tab.
> 
> Bootstrapped/regtested on x86_64-linux and i686-linux, ok for trunk?
> 
> 2022-10-27  Jakub Jelinek  <ja...@redhat.com>
> 
>       * include/std/charconv (__to_chars_float16_t, __to_chars_bfloat16_t):
>       Declare.
>       (to_chars): Add _Float16 and __gnu_cxx::__bfloat16_t overloads.
>       * config/abi/pre/gnu.ver (GLIBCXX_3.4.31): Export
>       _ZSt20__to_chars_float16_tPcS_fSt12chars_format and
>       _ZSt21__to_chars_bfloat16_tPcS_fSt12chars_format.
>       * src/c++17/floating_to_chars.cc (floating_type_float16_t,
>       floating_type_bfloat16_t): New types.
>       (floating_type_traits<floating_type_float16_t>,
>       floating_type_traits<floating_type_bfloat16_t>,
>       get_ieee_repr<floating_type_float16_t>,
>       get_ieee_repr<floating_type_bfloat16_t>,
>       __handle_special_value<floating_type_float16_t>,
>       __handle_special_value<floating_type_bfloat16_t>): New specializations.
>       (floating_to_shortest_scientific): Handle floating_type_float16_t
>       and floating_type_bfloat16_t like IEEE quad.
>       (__floating_to_chars_shortest): For floating_type_bfloat16_t call
>       __floating_to_chars_hex<float> rather than
>       __floating_to_chars_hex<floating_type_bfloat16_t> to avoid
>       instantiating the latter.
>       (__to_chars_float16_t, __to_chars_bfloat16_t): New functions.
> 
> --- libstdc++-v3/include/std/charconv.jj      2022-10-26 13:50:40.334716005 
> +0200
> +++ libstdc++-v3/include/std/charconv 2022-10-26 14:19:46.523769686 +0200
> @@ -738,6 +738,32 @@ namespace __detail
>    to_chars_result to_chars(char* __first, char* __last, long double __value,
>                          chars_format __fmt, int __precision) noexcept;
>  
> +  // Library routines for 16-bit extended floating point formats
> +  // using float as interchange format.
> +  to_chars_result __to_chars_float16_t(char* __first, char* __last,
> +                                    float __value,
> +                                    chars_format __fmt) noexcept;
> +  to_chars_result __to_chars_bfloat16_t(char* __first, char* __last,
> +                                     float __value,
> +                                     chars_format __fmt) noexcept;
> +
> +#if defined(__STDCPP_FLOAT16_T__) && defined(_GLIBCXX_FLOAT_IS_IEEE_BINARY32)
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last, _Float16 __value) noexcept
> +  {
> +    return __to_chars_float16_t(__first, __last, float(__value),
> +                             chars_format{});
> +  }
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last, _Float16 __value,
> +        chars_format __fmt) noexcept
> +  { return __to_chars_float16_t(__first, __last, float(__value), __fmt); }
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last, _Float16 __value,
> +        chars_format __fmt, int __precision) noexcept
> +  { return to_chars(__first, __last, float(__value), __fmt, __precision); }

FWIW when formatting as hex with explicit precision, the output is based
off of the shortest hex form, so going through the float32 overloads here
will mean that

  to_chars(1p-22f16, hex, 2)

outputs 0.01p-14 instead of 1.00p-22 I think.  But again this difference
in denormal hex output shouldn't cause any problems.

> +#endif
> +
>  #if defined(__STDCPP_FLOAT32_T__) && defined(_GLIBCXX_FLOAT_IS_IEEE_BINARY32)
>    inline to_chars_result
>    to_chars(char* __first, char* __last, _Float32 __value) noexcept
> @@ -784,6 +810,24 @@ namespace __detail
>                   __precision);
>    }
>  #endif
> +
> +#if defined(__STDCPP_BFLOAT16_T__) && 
> defined(_GLIBCXX_FLOAT_IS_IEEE_BINARY32)
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last,
> +        __gnu_cxx::__bfloat16_t __value) noexcept
> +  {
> +    return __to_chars_bfloat16_t(__first, __last, float(__value),
> +                              chars_format{});
> +  }
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last, __gnu_cxx::__bfloat16_t __value,
> +        chars_format __fmt) noexcept
> +  { return __to_chars_bfloat16_t(__first, __last, float(__value), __fmt); }
> +  inline to_chars_result
> +  to_chars(char* __first, char* __last, __gnu_cxx::__bfloat16_t __value,
> +        chars_format __fmt, int __precision) noexcept
> +  { return to_chars(__first, __last, float(__value), __fmt, __precision); }
> +#endif
>  #endif
>  
>  _GLIBCXX_END_NAMESPACE_VERSION
> --- libstdc++-v3/config/abi/pre/gnu.ver.jj    2022-09-12 11:30:14.211870202 
> +0200
> +++ libstdc++-v3/config/abi/pre/gnu.ver       2022-10-26 16:11:53.146300799 
> +0200
> @@ -2446,6 +2446,8 @@ GLIBCXX_3.4.30 {
>  
>  GLIBCXX_3.4.31 {
>      
> _ZNSt7__cxx1112basic_stringI[cw]St11char_traitsI[cw]ESaI[cw]EE15_M_replace_cold*;
> +    _ZSt20__to_chars_float16_tPcS_fSt12chars_format;
> +    _ZSt21__to_chars_bfloat16_tPcS_fSt12chars_format;
>  } GLIBCXX_3.4.30;
>  
>  # Symbols in the support library (libsupc++) have their own tag.
> --- libstdc++-v3/src/c++17/floating_to_chars.cc.jj    2022-05-20 
> 11:45:18.042741567 +0200
> +++ libstdc++-v3/src/c++17/floating_to_chars.cc       2022-10-26 
> 22:54:04.890144587 +0200
> @@ -374,6 +374,44 @@ namespace
>      };
>  #endif
>  
> +  // Wrappers around float for std::{,b}float16_t promoted to float.
> +  struct floating_type_float16_t
> +  {
> +    float x;
> +    operator float() const { return x; }
> +  };
> +  struct floating_type_bfloat16_t
> +  {
> +    float x;
> +    operator float() const { return x; }
> +  };
> +
> +  template<>
> +    struct floating_type_traits<floating_type_float16_t>
> +    {
> +      static constexpr int mantissa_bits = 10;
> +      static constexpr int exponent_bits = 5;
> +      static constexpr bool has_implicit_leading_bit = true;
> +      using mantissa_t = uint32_t;
> +      using shortest_scientific_t = ryu::floating_decimal_128;
> +
> +      static constexpr uint64_t pow10_adjustment_tab[]
> +     = { 0 };
> +    };
> +
> +  template<>
> +    struct floating_type_traits<floating_type_bfloat16_t>
> +    {
> +      static constexpr int mantissa_bits = 7;
> +      static constexpr int exponent_bits = 8;
> +      static constexpr bool has_implicit_leading_bit = true;
> +      using mantissa_t = uint32_t;
> +      using shortest_scientific_t = ryu::floating_decimal_128;
> +
> +      static constexpr uint64_t pow10_adjustment_tab[]
> +     = { 0b0000111001110001101010010110100101010010000000000000000000000000 
> };
> +    };
> +
>    // An IEEE-style decomposition of a floating-point value of type T.
>    template<typename T>
>      struct ieee_t
> @@ -482,6 +520,79 @@ namespace
>      }
>  #endif
>  
> +  template<>
> +    ieee_t<floating_type_float16_t>
> +    get_ieee_repr(const floating_type_float16_t value)
> +    {
> +      using mantissa_t = typename floating_type_traits<float>::mantissa_t;
> +      constexpr int mantissa_bits = 
> floating_type_traits<float>::mantissa_bits;
> +      constexpr int exponent_bits = 
> floating_type_traits<float>::exponent_bits;
> +
> +      uint32_t value_bits = 0;
> +      memcpy(&value_bits, &value.x, sizeof(value));
> +
> +      ieee_t<floating_type_float16_t> ieee_repr;
> +      ieee_repr.mantissa
> +     = static_cast<mantissa_t>(value_bits & ((uint32_t{1} << mantissa_bits) 
> - 1u));
> +      value_bits >>= mantissa_bits;
> +      ieee_repr.biased_exponent
> +     = static_cast<uint32_t>(value_bits & ((uint32_t{1} << exponent_bits) - 
> 1u));
> +      value_bits >>= exponent_bits;
> +      ieee_repr.sign = (value_bits & 1) != 0;
> +      // We have mantissa and biased_exponent from the float (originally
> +      // float16_t converted to float).
> +      // Transform that to float16_t mantissa and biased_exponent.
> +      // If biased_exponent is 0, then value is +-0.0.
> +      // If biased_exponent is 0x67..0x70, then it is a float16_t denormal.
> +      if (ieee_repr.biased_exponent >= 0x67
> +       && ieee_repr.biased_exponent <= 0x70)
> +     {
> +       int n = ieee_repr.biased_exponent - 0x67;
> +       ieee_repr.mantissa = ((uint32_t{1} << n)
> +                             | (ieee_repr.mantissa >> (mantissa_bits - n)));
> +       ieee_repr.biased_exponent = 0;
> +     }
> +      // If biased_exponent is 0xff, then it is a float16_t inf or NaN.
> +      else if (ieee_repr.biased_exponent == 0xff)
> +     {
> +       ieee_repr.mantissa >>= 13;
> +       ieee_repr.biased_exponent = 0x1f;
> +     }
> +      // If biased_exponent is 0x71..0x8e, then it is a float16_t normal 
> number.
> +      else if (ieee_repr.biased_exponent > 0x70)
> +     {
> +       ieee_repr.mantissa >>= 13;
> +       ieee_repr.biased_exponent -= 0x70;
> +     }
> +      return ieee_repr;
> +    }
> +
> +  template<>
> +    ieee_t<floating_type_bfloat16_t>
> +    get_ieee_repr(const floating_type_bfloat16_t value)
> +    {
> +      using mantissa_t = typename floating_type_traits<float>::mantissa_t;
> +      constexpr int mantissa_bits = 
> floating_type_traits<float>::mantissa_bits;
> +      constexpr int exponent_bits = 
> floating_type_traits<float>::exponent_bits;
> +
> +      uint32_t value_bits = 0;
> +      memcpy(&value_bits, &value.x, sizeof(value));
> +
> +      ieee_t<floating_type_bfloat16_t> ieee_repr;
> +      ieee_repr.mantissa
> +     = static_cast<mantissa_t>(value_bits & ((uint32_t{1} << mantissa_bits) 
> - 1u));
> +      value_bits >>= mantissa_bits;
> +      ieee_repr.biased_exponent
> +     = static_cast<uint32_t>(value_bits & ((uint32_t{1} << exponent_bits) - 
> 1u));
> +      value_bits >>= exponent_bits;
> +      ieee_repr.sign = (value_bits & 1) != 0;
> +      // We have mantissa and biased_exponent from the float (originally
> +      // bfloat16_t converted to float).
> +      // Transform that to bfloat16_t mantissa and biased_exponent.
> +      ieee_repr.mantissa >>= 16;
> +      return ieee_repr;
> +    }
> +
>    // Invoke Ryu to obtain the shortest scientific form for the given
>    // floating-point number.
>    template<typename T>
> @@ -493,7 +604,9 @@ namespace
>        else if constexpr (std::is_same_v<T, double>)
>       return ryu::floating_to_fd64(value);
>        else if constexpr (std::is_same_v<T, long double>
> -                      || std::is_same_v<T, F128_type>)
> +                      || std::is_same_v<T, F128_type>
> +                      || std::is_same_v<T, floating_type_float16_t>
> +                      || std::is_same_v<T, floating_type_bfloat16_t>)
>       {
>         constexpr int mantissa_bits
>           = floating_type_traits<T>::mantissa_bits;
> @@ -678,6 +791,28 @@ template<typename T>
>      return {{first, errc{}}};
>    }
>  
> +template<>
> +  optional<to_chars_result>
> +  __handle_special_value<floating_type_float16_t>(char* first,
> +                                               char* const last,
> +                                               const floating_type_float16_t 
> value,
> +                                               const chars_format fmt,
> +                                               const int precision)
> +  {
> +    return __handle_special_value(first, last, value.x, fmt, precision);
> +  }
> +
> +template<>
> +  optional<to_chars_result>
> +  __handle_special_value<floating_type_bfloat16_t>(char* first,
> +                                                char* const last,
> +                                                const 
> floating_type_bfloat16_t value,
> +                                                const chars_format fmt,
> +                                                const int precision)
> +  {
> +    return __handle_special_value(first, last, value.x, fmt, precision);
> +  }
> +
>  // This subroutine of the floating-point to_chars overloads performs
>  // hexadecimal formatting.
>  template<typename T>
> @@ -922,7 +1057,15 @@ template<typename T>
>                              chars_format fmt)
>    {
>      if (fmt == chars_format::hex)
> -      return __floating_to_chars_hex(first, last, value, nullopt);
> +      {
> +     // 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>)
> +       return __floating_to_chars_hex(first, last, value.x, nullopt);

In light of the above, I'm inclined to suggest we might as well go
through float for the shortest hex formatting of float16 too.

> +     else
> +       return __floating_to_chars_hex(first, last, value, nullopt);
> +      }
>  
>      __glibcxx_assert(fmt == chars_format::fixed
>                    || fmt == chars_format::scientific
> @@ -1662,6 +1805,23 @@ to_chars(char* first, char* last, __floa
>  }
>  #endif
>  
> +// Entrypoints for 16-bit floats.
> +[[gnu::cold]] to_chars_result
> +__to_chars_float16_t(char* first, char* last, float value,
> +                  chars_format fmt) noexcept
> +{
> +  return __floating_to_chars_shortest(first, last,
> +                                   floating_type_float16_t{ value }, fmt);
> +}
> +
> +[[gnu::cold]] to_chars_result
> +__to_chars_bfloat16_t(char* first, char* last, float value,
> +                   chars_format fmt) noexcept
> +{
> +  return __floating_to_chars_shortest(first, last,
> +                                   floating_type_bfloat16_t{ value }, fmt);
> +}
> +
>  #ifdef _GLIBCXX_LONG_DOUBLE_COMPAT
>  // Map the -mlong-double-64 long double overloads to the double overloads.
>  extern "C" to_chars_result
> 
>       Jakub
> 

Reply via email to