Fix std::float16_t rounding of subnormal ties and fast-path overflow - #408
Open
jadidbourbaki wants to merge 2 commits into
Open
jadidbourbaki wants to merge 2 commits into
jadidbourbaki wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This addresses #300.
compute_floatrounded subnormal ties up instead of to even. For float and double this never shows because no subnormal tie fits in 19 digits. Forstd::float16_tthe ties (2k+1) * 2^-25 up to 33 * 2^-25 do. So2.98023223876953125e-8, which is exactly 2^-25, parsed as the smallest subnormal instead of zero. Nine more ties with an even lower neighbour parsed as the odd neighbour. The same strings padded with zeros past 19 digits were wrong too, because thetoo_many_digitscheck comparescompute_float(w)withcompute_float(w + 1)and both rounded up. I added the round-to-even test from the normal branch to the subnormal branch. I also widenedmin_exponent_round_to_evenfor float16 from -22 to -26, which is where the subnormal ties sit. The test is gated on a compile-time predicate,subnormal_ties_possible(), so that it only exists in the float16 instantiation.Clinger's fast path can overflow for float16 because 2^11 * 10^4 exceeds 65504. Strings such as
656e2returned infinity withstd::errc()while every other path reportsresult_out_of_range. I addedbinary_format<T>::fast_path_can_overflow(), which is true only for float16. The fast path now hands such inputs to the slow path. The check folds to false for the other types.The double
max_mantissatable skipped the 5^14 entry, so indices 14 to 22 were one power of five too small. Results were unaffected. The non-nearest fast path was taken less often than intended. I added the missing entry.I went through every
binary_formatconstant for the four types and wrote down the condition each one must satisfy. Three were conservative and I tightened them:smallest_power_of_tenfor float16 (-27 to -26) and bfloat16 (-60 to -59), andmin_exponent_fast_pathfor float16 (0 to -4) and bfloat16 (0 to -3). The division fast path is safe for the 16-bit types even when the compiler evaluates the quotient in float, double or x87 long double. I checked that exhaustively for every w up to 2^(p+1) and every k.max_digitsis one larger than necessary for double, float and bfloat16 and I left it alone.script/format_parameters.pyreads the constants out offloat_common.hand checks each condition with exact integer arithmetic, in the spirit ofscript/mushtak_lemire.py. It also runs a bit-exact model ofcompute_floatfor the 16-bit types against a ties-to-even reference on every (w, q) within three of a representable value, a tie or the overflow threshold. It reruns the Mushtak-Lemire convergent search over each type's exponent range. bfloat16 has no subnormal tie under 20 digits and no wrong results.Testing
tests/exhaustive16_midpoint.cppparses the exact decimal expansion of every finite float16 and bfloat16 value, every midpoint, strings just above and below each midpoint, and midpoints padded past 19 digits. The expected bits come from the bit pattern that generated the string. It runs in under a second and is registered underFASTFLOAT_FIXEDWIDTH_TESTS. Against the unmodified headers it reports 18 failures.tests/basictest.cppfor the ties and for the overflow error code. The constexpr variants of the tie cases fail to compile against the unmodified headers.basictest,fixedwidthtestand the rest of the suite pass with GCC 16 in C++23 mode.exhaustive32,exhaustive32_midpointandexhaustive32_64pass, so float is unchanged by the sharedcompute_floatedit. The headers build under Apple clang in C++11, 17 and 20.python3 script/format_parameters.pyreports zero failures for all four types.Benchmarks
Apple M4 Pro, macOS 26.5, base commit a8a02f7 against this branch, interleaved runs, minimum of 1000 repetitions per run.
For float and double I compiled a translation unit calling
from_charswith both header sets and diffed the assembly (Apple clang 16 and GCC 16,-O3). The only differences are the correctedmax_mantissaconstants.realbenchmarkon canada and mesh agrees, with every difference inside the run-to-run noise:realbenchmarkhas no float16 path, so I timedfrom_chars<std::float16_t>with the same loop shape (GCC 16,-O3) on two inputs: every finite positive float16 printed as its shortest decimal (31,743 lines), and 100,000 normally distributed values rounded to float16, also printed shortest. Best of three interleaved runs:The float control in the same binaries moved by up to 4 percent between runs, so I read these as no change. Few of these inputs take the division fast path anyway: a shortest float16 decimal usually has four or five significant digits, and the fast path needs w <= 2048.
Future Work
I am working on a formal analysis in Lean of
compute_floatfor the 16-bit types, with the truncation and tie lemmas machine-checked instead of argued in comments. The constants that reduce to inequalities on small integers could also becomestatic_asserts.