Clang generated ~35% more instructions per float than GCC on identical
code (canada.txt, 64-bit: 276 vs 200 i/f on a Xeon Gold 6548N). Two
causes, confirmed with exact instruction traces:
1. Clang declines to inline the parser (from_chars_caller<T>::call) into
its callers, so parse_options stays a runtime value and every format
flag is re-tested on every conversion: javascript, skip_white_space,
allow_leading_plus, fixed/scientific, the decimal point, the JSON
dispatch. GCC inlines the whole parser into each call site, where
chars_format::general is a constant, and all of it folds away.
Fix: from_chars dispatches chars_format::general to a
from_chars_fixed_format<T, UC, Fmt> instantiation, so the format is a
compile-time constant inside the parser body whether or not the user's
call is inlined. The call forwarder gets fastfloat_clang_really_inline
so that it folds into that instantiation.
2. Clang if-converts the rare over/underflow fix-ups at the end of
compute_float and the result_out_of_range test into cmov chains that
every conversion executes. Marking them fastfloat_clang_unlikely keeps
them as never-taken branches.
Both are applied under clang only, and GCC's generated code is identical
to before (verified on the object code). That is on purpose: forcing
always_inline on from_chars, or even on the trivial call forwarder,
regressed GCC by up to 14% on mesh.txt by changing its inlining order;
dispatching to the fixed-format instantiation made it stop inlining into
the loop; and the unlikely hints alone cost it 2-6% on mesh.txt by
reordering blocks (GCC already emits branches there).
realbenchmark, cycles per float, 5 interleaved rounds, medians:
clang 21, Xeon Gold 6548N before after
canada.txt ASCII (64) 49.7 44.6 -10%
canada.txt UTF-16 (64) 57.4 52.0 -9%
mesh.txt ASCII (64) 26.5 22.4 -16%
mesh.txt UTF-16 (64) 28.1 24.0 -15%
(32-bit: -8% to -14%)
gcc 14, same machine: generated code unchanged.
Apple clang 17, arm64: +3% to +18% Mfloat/s.
PR #356 replaced the 3/5-character case-insensitive compare used by
parse_infnan with SWAR helpers (fastfloat_strncasecmp3/5 plus a generic
memcpy-based fastfloat_strncasecmp). That code only runs for "nan",
"inf" and "infinity" inputs, so it cannot help ordinary parsing, but it
grew the inlined inf/nan tail of the parser: on x86-64 (GCC 14 and
clang 21) the per-commit benchmarks dropped by 1.5-2.5% because the
extra dead code changed inlining decisions and register allocation on
the hot path (+7 instructions/float on canada.txt).
Go back to the plain loop, which the compiler unrolls for the constant
lengths 3 and 5, and drop the now-unused generic SWAR function. The
`actual < 256` guard of the original is unnecessary: OR-ing 0x20 into
a value can only equal an ASCII lowercase letter when the value is that
letter or its uppercase form, so the plain OR is exact for every
character type.
8.3.0 added the ECMAScript leading-zero rule as a runtime test on fmt inside
parse_number_string, so unlike basic_json_fmt its error returns and Annex B
octal scan land in the parser body that every from_chars instantiation
force-inlines. from_chars_caller<double>::call grew from 935 to 1031
instructions and other formats lost about 3% on short inputs.
Make the rule a compile-time parameter of the parser body, selected once at the
entry point, and take chars_format::javascript out to a cold conversion before
anything else is live so the branch is a tail call. parse_number_string keeps
its signature and behaviour.
benchmarks/benchmark.cpp, Intel Xeon 6975P-C, clang 22.1.8, cycles per float,
best of 7:
canada.txt mesh.txt
8.2.10 47.03 24.78
8.3.0 47.34 26.08
this 46.28 24.86
chars_format::javascript follows the strict-mode grammar, which rejects
any leading zero. In sloppy mode, JavaScript also accepts a leading zero
when one of the digits is 8 or 9 ("08.5" is 8.5, Annex B's
NonOctalDecimalIntegerLiteral). A leading zero followed only by octal
digits is a LegacyOctalIntegerLiteral ("0775"), which is not a decimal
number: it is rejected with the new parse_error::legacy_octal_integer_part
so the caller can parse it in base 8.
javascript_sloppy sets one extra bit on top of javascript, and the check
lives inside the existing runtime JavaScript branch of
parse_number_string<false>, so the general path is unchanged. An
interleaved A/B with realbenchmark on canada.txt and mesh.txt (ASCII and
UTF-16, 32/64-bit) shows identical instructions per float and cycles
within noise.
Document both formats in the README.
The JSON format rejects leading zeros, which JavaScript also rejects,
but it further requires digits on both sides of the decimal point,
which JavaScript does not: ".5", "5." and "5.e3" are all valid
DecimalLiteral productions. Users lexing JavaScript therefore could not
use chars_format::json and had to re-check the integer span themselves
after a general-mode parse.
chars_format::javascript is general mode plus the leading-zero rule
(reported as leading_zeros_in_integer_part), with no_infnan. It is a
runtime check on the format, like fortran, so it reuses the existing
parse_number_string<false> instantiation. A leading minus sign is
accepted, and a leading plus sign with allow_leading_plus.
Claude-Session: https://claude.ai/code/session_01KbAWRBpE3MPy5cXECc5vWZ