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

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 commit is contained in:
Daniel Lemire 2026-09-21 22:19:35 -04:00
parent e9e04dec19
commit b57a12270e
3 changed files with 71 additions and 11 deletions

View File

@ -194,16 +194,31 @@ compute_float(int64_t q, uint64_t w) noexcept {
answer.mantissa += (answer.mantissa & 1); // round up
answer.mantissa >>= 1;
if (answer.mantissa >= (uint64_t(2) << binary::mantissa_explicit_bits())) {
// Both fix-ups below are rare. They are marked unlikely so that clang keeps
// them as branches instead of folding them into conditional moves that
// every conversion pays for.
#ifdef __clang__
#pragma clang diagnostic push
#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13)
#pragma clang diagnostic ignored "-Wc++20-extensions"
#endif
#endif
if fastfloat_clang_unlikely (answer.mantissa >=
(uint64_t(2)
<< binary::mantissa_explicit_bits())) {
answer.mantissa = (uint64_t(1) << binary::mantissa_explicit_bits());
answer.power2++; // undo previous addition
}
answer.mantissa &= ~(uint64_t(1) << binary::mantissa_explicit_bits());
if (answer.power2 >= binary::infinite_power()) { // infinity
if fastfloat_clang_unlikely (answer.power2 >= binary::infinite_power()) {
// infinity
answer.power2 = binary::infinite_power();
answer.mantissa = 0;
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
return answer;
}

View File

@ -228,6 +228,27 @@ using parse_options = parse_options_t<char>;
#define fastfloat_never_inline
#endif
// fastfloat_really_inline under clang only, a no-op elsewhere. Clang declines
// to inline the parser into its callers, so the parser instantiation with a
// compile-time format (from_chars_fixed_format) must force the one thin
// forwarder between it and the parser body. GCC inlines the chain on its own,
// and forcing it there changes its inlining order for the worse.
#ifdef __clang__
#define fastfloat_clang_really_inline fastfloat_really_inline
#else
#define fastfloat_clang_really_inline
#endif
// fastfloat_unlikely under clang only, a plain condition elsewhere. Clang
// if-converts rare checks into chains of conditional moves that every
// conversion executes; the hint keeps them as branches. GCC already emits
// branches there, and the hint only reorders its blocks, for the worse.
#ifdef __clang__
#define fastfloat_clang_unlikely(x) fastfloat_unlikely(x)
#else
#define fastfloat_clang_unlikely(x) (x)
#endif
// Branch-probability hint marking the rare slow-path branches as cold, so the
// optimizer keeps the out-of-line slow-path re-parse off the hot path (and does
// not duplicate the force-inlined hot scanner into the caller, which bloated

View File

@ -139,9 +139,10 @@ fastfloat_really_inline bool rounds_to_nearest() noexcept {
template <typename T> struct from_chars_caller {
template <typename UC>
FASTFLOAT_CONSTEXPR20 static from_chars_result_t<UC>
call(UC const *first, UC const *last, T &value,
parse_options_t<UC> options) noexcept {
fastfloat_clang_really_inline
FASTFLOAT_CONSTEXPR20 static from_chars_result_t<UC>
call(UC const *first, UC const *last, T &value,
parse_options_t<UC> options) noexcept {
return from_chars_advanced(first, last, value, options);
}
};
@ -180,10 +181,29 @@ template <> struct from_chars_caller<std::float64_t> {
};
#endif
#ifdef __clang__
// Parser instantiated for a format fixed at compile time, so that every test
// on the format folds away inside it. GCC gets the same effect by inlining the
// whole parser into each caller, where the format is a constant; clang keeps
// it out of line and would otherwise re-test each format flag per conversion.
template <typename T, typename UC, chars_format Fmt>
FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
from_chars_fixed_format(UC const *first, UC const *last, T &value) noexcept {
return from_chars_caller<T>::call(first, last, value,
parse_options_t<UC>(Fmt));
}
#endif
template <typename T, typename UC, typename>
FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
from_chars(UC const *first, UC const *last, T &value,
chars_format fmt /*= chars_format::general*/) noexcept {
#ifdef __clang__
if (fmt == chars_format::general) {
return from_chars_fixed_format<T, UC, chars_format::general>(first, last,
value);
}
#endif
return from_chars_caller<T>::call(first, last, value,
parse_options_t<UC>(fmt));
}
@ -428,15 +448,19 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
if fastfloat_unlikely (am.power2 < 0) {
return parse_number_slow_path<T, UC>(first, last, value, options, bjf);
}
to_float(pns.negative, am, value);
// Test for over/underflow. Marked unlikely so that clang keeps it as a
// branch instead of folding it into a chain of conditional moves that
// every conversion pays for.
if fastfloat_clang_unlikely ((pns.mantissa != 0 && am.mantissa == 0 &&
am.power2 == 0) ||
am.power2 ==
binary_format<T>::infinite_power()) {
answer.ec = std::errc::result_out_of_range;
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
to_float(pns.negative, am, value);
// Test for over/underflow.
if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) ||
am.power2 == binary_format<T>::infinite_power()) {
answer.ec = std::errc::result_out_of_range;
}
return answer;
}