public inbox for libstdc++@gcc.gnu.org
 help / color / mirror / Atom feed
From: Patrick Palka <ppalka@redhat.com>
To: Jakub Jelinek <jakub@redhat.com>
Cc: Jonathan Wakely <jwakely@redhat.com>,
	Patrick Palka <ppalka@redhat.com>,
	 gcc-patches@gcc.gnu.org, libstdc++@gcc.gnu.org
Subject: Re: [PATCH] libstdc++: std::to_chars std::{,b}float16_t support
Date: Fri, 28 Oct 2022 12:52:44 -0400 (EDT)	[thread overview]
Message-ID: <4ba36955-3f9a-00c5-c406-45821ec2a4db@idea> (raw)
In-Reply-To: <Y1o6fhwgbVZoh4Pe@tucnak>

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  <jakub@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	other threads:[~2022-10-28 16:52 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-10-27  7:59 Jakub Jelinek
2022-10-28 16:52 ` Patrick Palka [this message]
2022-10-28 17:16   ` Jakub Jelinek
2022-11-01 12:18   ` [PATCH] libstdc++: Shortest denormal hex std::to_chars Jakub Jelinek
2022-11-01 12:24     ` Jonathan Wakely
2022-11-01 13:46       ` Patrick Palka
2022-11-01 12:22 ` [PATCH] libstdc++: std::to_chars std::{,b}float16_t support Jonathan Wakely

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=4ba36955-3f9a-00c5-c406-45821ec2a4db@idea \
    --to=ppalka@redhat.com \
    --cc=gcc-patches@gcc.gnu.org \
    --cc=jakub@redhat.com \
    --cc=jwakely@redhat.com \
    --cc=libstdc++@gcc.gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).