Skip to content

Fold the format at compile time under clang, mark cold checks unlikely - #413

Merged
lemire merged 1 commit into
mainfrom
clang-format-fold
Sep 22, 2026
Merged

lemire merged 1 commit into
mainfrom
clang-format-fold

Conversation

@lemire

@lemire lemire commented Sep 22, 2026

Copy link
Copy Markdown
Member

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 (gdb stepi on 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, so parse_options is 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 input 1 costs clang 94 instructions to GCC's 57. GCC inlines the whole parser into each call site (even with several sites in the TU), where chars_format::general is a constant, and all of that 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 gets inlined. The call forwarder gets fastfloat_clang_really_inline so 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 the result_out_of_range test compile to ~43 instructions of cmov/setne under clang, executed on every conversion; GCC emits 17 with never-taken branches. Marking them fastfloat_clang_unlikely keeps them as branches.

Why clang-only

Both changes are under #ifdef __clang__, and GCC's generated code is identical to main (verified on the object code). That is deliberate; each GCC-inclusive variant was measured with realbenchmark and lost:

  • always_inline on from_chars, or even on the trivial call forwarder: GCC −2% to −14% (mesh.txt 64-bit 16.6 → 18.9 cycles/float), by changing its early/late inlining order.
  • dispatching GCC to the fixed-format instantiation: it stops inlining the parser into the loop, mesh.txt +38%.
  • the unlikely hints 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 is main at e9e04de.

clang 21.1, Xeon Gold 6548N

canada ASCII canada UTF-16 mesh ASCII mesh UTF-16
64-bit 49.7 → 44.6 (−10%) 57.4 → 52.0 (−9%) 26.5 → 22.4 (−16%) 28.1 → 24.0 (−15%)
32-bit 49.2 → 45.1 (−8%) 56.1 → 51.7 (−8%) 30.4 → 26.9 (−11%) 32.8 → 28.2 (−14%)

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%).

ctest passes with clang 21 and GCC 14; tests/javascript_fmt.cpp also 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; the FASTFLOAT_ASSERT macros it reflows differently from clang-format 17 were left untouched.

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.
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.

1 participant