Fold the format at compile time under clang, mark cold checks unlikely - #413
Merged
Merged
Conversation
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.
This was referenced Sep 22, 2026
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.
Clang generates ~35% more instructions per float than GCC on identical code: on canada.txt, 64-bit, 276 vs 200 instructions/float (Xeon Gold 6548N, clang 21.1 vs GCC 14.3). This is not
-march(GCC at baseline x86-64 is still 207 vs 282) and no clang flag changes it. Exact instruction traces (gdbstepion one call) show two causes.1. The format never constant-folds under clang (~30 i/f)
Clang declines to inline the ~3.5 KB parser (
from_chars_caller<T>::call) into its callers, soparse_optionsis passed at runtime 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. Even the input1costs clang 94 instructions to GCC's 57. GCC inlines the whole parser into each call site (even with several sites in the TU), wherechars_format::generalis a constant, and all of that folds away.Fix:
from_charsdispatcheschars_format::generalto afrom_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 gets inlined. Thecallforwarder getsfastfloat_clang_really_inlineso it folds into that instantiation. The instantiation shrinks from 3.5 KB to 2.0 KB.2. Cold checks if-converted into cmov chains (~20 i/f)
The rare fix-ups at the end of
compute_float(mantissa carry, infinity clamp) and theresult_out_of_rangetest compile to ~43 instructions ofcmov/setneunder clang, executed on every conversion; GCC emits 17 with never-taken branches. Marking themfastfloat_clang_unlikelykeeps them as branches.Why clang-only
Both changes are under
#ifdef __clang__, and GCC's generated code is identical tomain(verified on the object code). That is deliberate; each GCC-inclusive variant was measured withrealbenchmarkand lost:always_inlineonfrom_chars, or even on the trivialcallforwarder: GCC −2% to −14% (mesh.txt 64-bit 16.6 → 18.9 cycles/float), by changing its early/late inlining order.unlikelyhints alone: GCC 2–6% slower on mesh.txt with an identical instruction count (block re-layout).Measurements
realbenchmark, cycles per float, 5 interleaved rounds pinned to one core, medians. Baseline ismainat e9e04de.clang 21.1, Xeon Gold 6548N
Instructions/float on canada 64-bit: 276 → 243.
GCC 14.3, same machine: generated code unchanged.
Apple clang 17, arm64: +3% to +18% Mfloat/s (mesh.txt +12% to +18%, canada.txt +3% to +9%).
ctestpasses with clang 21 and GCC 14;tests/javascript_fmt.cppalso compiled and run by hand under clang in C++11 and C++20 (the[[unlikely]]path), no warnings with-Wall -Wextra. Formatted with clang-format 20; theFASTFLOAT_ASSERTmacros it reflows differently from clang-format 17 were left untouched.