https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127980

            Bug ID: 127980
           Summary: std::from_chars rounds std::float16_t subnormal ties
                    to odd
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: spencerpages.mail at gmail dot com
  Target Milestone: ---

Created attachment 65883
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65883&action=edit
testcase

For the smallest std::float16_t subnormal ties, (2i + 1/2) * 2^-24 with
i = 0..8, std::from_chars returns the odd neighbor instead of the even one.

Testcase (attached), g++ -std=c++23 t.cc && ./a.out:

  0.0000000298023223876953125    -> 0x0001 (ec 0)  want 0x0000  WRONG
  0.0000001490116119384765625    -> 0x0003 (ec 0)  want 0x0002  WRONG
  0.0000002682209014892578125    -> 0x0005 (ec 0)  want 0x0004  WRONG
  0.000000059604644775390625     -> 0x0001 (ec 0)  want 0x0001  ok

The first input is exactly 2^-25, halfway between 0 and the smallest
subnormal 2^-24, so it must round to 0 (even). The second is exactly
2.5 * 2^-24 and must round to 2 * 2^-24.

Same output with GCC 14.4.0 and 16.2.0 (official gcc:14 and gcc:16 images).
An exhaustive sweep of all 31,743 binary16 midpoints (GCC 14.4.0) finds
exactly 9 wrong,
i = 0..8, i.e. (4i+1) * 2^-25 up to 33 * 2^-25. Every normal-range tie is
right, and std::bfloat16_t, float and double have no rounding errors.

Cause: compute_float() in src/c++17/fast_float/fast_float.h rounds the
subnormal branch up unconditionally:

    answer.mantissa >>= -answer.power2 + 1;
    // Thankfully, we can't have both "round-to-even" and subnormals because
    // "round-to-even" only occurs for powers close to 0.
    answer.mantissa += (answer.mantissa & 1); // round up

That assumption holds for float and double (no subnormal tie has 19 or fewer
significant digits) but not for a 16-bit format: 2^-25 is
298023223876953125e-25. Upstream fast_float fixed exactly this in
https://github.com/fastfloat/fast_float/pull/408 (merged 2026-09-22) by
applying the normal branch's round-to-even test in the subnormal branch too.

A re-sync of fast_float alone will not fix libstdc++, because libstdc++'s
float16 support is its own wrapper (floating_type_float16_t in
src/c++17/floating_from_chars.cc), not upstream's std::float16_t
specialization. The fix needs both:

  1. the subnormal round-to-even test from fast_float#408 in compute_float()
     (upstream gates it on binary_format<T>::subnormal_ties_possible(), which
     is derived from the format's constants and already comes out true for
     floating_type_float16_t), and
  2. binary_format<floating_type_float16_t>::min_exponent_round_to_even()
     widened from -22 to -26, as upstream did for its float16_t, since the
     subnormal ties need q = -25 and q = -26. Without this, the check in (1)
     is never reached for these inputs.

The bundled copy was last merged from upstream in November 2022 (PR107468,
MERGE = 2ef9abbcf6a1), so it predates the fix.

Use the following links to see the expected conversion results:

  2^-25 (binary64) -> binary16:
 
https://sw23.github.io/fp-conv/?in=fp64&out=fp16&val=0.0000000298023223876953125
  2.5 * 2^-24 (binary64) -> binary16:
 
https://sw23.github.io/fp-conv/?in=fp64&out=fp16&val=0.0000001490116119384765625
  4.5 * 2^-24 (binary64) -> binary16:
 
https://sw23.github.io/fp-conv/?in=fp64&out=fp16&val=0.0000002682209014892578125

This bug was found comparing the results of several open-source floating point
conversion projects. A LLM helped prepare the test cases and this write-up.
  • [Bug libstdc++/127980] New... spencerpages.mail at gmail dot com via Gcc-bugs

Reply via email to