Skip to content

Add performance hints: unpredictable_sign, assume_round_to_nearest, short_numbers - #434

Open
lemire wants to merge 2 commits into
mainfrom
performance-hints
Open

lemire wants to merge 2 commits into
mainfrom
performance-hints

Conversation

@lemire

@lemire lemire commented Nov 10, 2026

Copy link
Copy Markdown
Member

Users can now tell fast_float what to expect from their data or their program, so that it parses faster. The hints are a template argument; nothing else changes:

using fast_float::performance_hints;
fast_float::from_chars<performance_hints::unpredictable_sign>(first, last, value);
fast_float::from_chars<performance_hints::short_numbers |
                       performance_hints::assume_round_to_nearest>(
    first, last, value, fast_float::chars_format::fixed);
fast_float::from_chars_advanced<hints>(first, last, value, options);
fast_float::integer_times_pow10<hints>(mantissa, exponent);        // double
fast_float::integer_times_pow10<float, hints>(mantissa, exponent);

Without hints, the code is that of main: existing calls compile and run as before.

The hints

  • unpredictable_sign: the sign is handled without a branch, in the parser and in Clinger's fast path (which then multiplies or divides by a signed power of ten). This helps when positive and negative numbers come in random order (e.g., gains and losses), and costs when the signs are predictable.
  • assume_round_to_nearest: fast_float does not check the rounding mode before Clinger's fast path. It replaces the FASTFLOAT_ROUNDS_TO_NEAREST macro (added after v8.3.3 and never released), which is removed. Unlike the macro, it applies call by call and cannot cause ODR violations.
  • short_numbers: when the range [first, last) holds a single number of at most 8 characters without an exponent (e.g., -12.5), it is parsed with a few bitwise operations (SWAR) in the caller, with no branch on its shape. This helps when the shapes vary unpredictably and costs when they are predictable. Only for char inputs and float/double.

The hints are a separate type (fast_float::performance_hints) rather than chars_format flags, so they cannot be passed at run time by mistake. The JSON and JavaScript formats ignore unpredictable_sign and short_numbers.

Implementation notes

  • The choices are made at compile time (template parameters, if constexpr in C++17): given an ordinary if, GCC turns the branch-free sign skip back into a branch on the sign. The sign is detected arithmetically, ((c ^ '-') - 1) >> 63, for the same reason.
  • With Clang, the hinted parser is instantiated for chars_format::general and kept out of line, as main does for the default parser. With GCC, the format is passed as a constant argument so that GCC inlines the parser as it does by default (forcing the inlining made it slower).
  • integer_times_pow10: Clinger's fast path is inlined and the rest is out of line.

Benchmarks

realbenchmark gains a data set of 100,000 prices with random signs and two decimals, and fastfloat (64, unpredictable sign) lines. The hinted loops are in benchmarks/benchmark_hints.cpp, which replaces benchmark_rtn.cpp: in the same file, the extra instantiations exhaust GCC's inlining budget and slow down the default loops.

Intel Xeon Gold 6338, fastfloat (64), MB/s, ASCII / UTF-16 (best of two runs):

GCC 15 main GCC 15 PR GCC 15 unpredictable_sign Clang 21 main Clang 21 PR Clang 21 unpredictable_sign
canada 1168 / 1916 1205 / 1920 1088 / 1761 1025 / 1703 1033 / 1682 940 / 1562
mesh 1088 / 1869 1109 / 1880 899 / 1532 854 / 1537 842 / 1555 766 / 1373
prices — 559 / 1106 702 / 1311 — 524 / 1047 666 / 1301

(main has no prices data set; the PR's default line runs the same code.) assume_round_to_nearest performs as the macro did (within 2–4%).

On short numbers with varying shapes (500,000 numbers such as 7, -12.5, 1042), short_numbers takes the time per number from 16.1 to 10.8 ns (GCC 15) and from 18.2 to 11.0 ns (Clang 21) on the same Xeon, and from 7.5 to 3.2 ns on an Apple M4 (with all three hints). On short numbers with predictable shapes, it is slower: hence a hint.

Tests

tests/performance_hints_test.cpp checks that every combination of hints gives the same value, error and end pointer as no hint, for many formats, char/char16_t/char32_t, float/double, from_chars_advanced with a comma as decimal point, and integer_times_pow10 with every integer type; that assume_round_to_nearest agrees with strtod; and that the existing overloads (explicit types, function pointers, integers) still resolve. It replaces rounds_to_nearest_test.cpp. Also checked: GCC C++11/C++20 (constexpr tests)/C++23 and Clang C++17, without warnings, and fuzzing against main.

Documentation

The README gains a "Performance hints" section (which absorbs "Assuming the default rounding mode") and an example, "Parsing formatted numbers in one pass", that parses $1,234.56 or -12.5% with integer_times_pow10 (tests/example_formatted.cpp).

@LeszekSwirski

Copy link
Copy Markdown
Collaborator

It could be an interesting idea to offer a not_short_numbers performance hint, for cases where a short number was checked already in an inline path that wrote to an int32 (this is what happens in V8's use of fast_float).

@lemire

lemire commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

@LeszekSwirski I like it, let us see whether this could be beneficial.

@lemire

lemire commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

@LeszekSwirski I have opened it as an issue for research.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants