Skip to content

Commit cb5d9cd

Browse files
Skip materializing the integer/fraction spans on the hot path
parsed_number_string_t carries two span<UC const> members (integer, fraction) that are only read on the rare slow paths (digit_comp, and the >19-significant- digit truncation recompute). Materializing them on every parse forces the ~56/64- byte struct to be written out and marshaled through the by-value return, which shows up as backend/store pressure on the hot path. This adds a runtime `store_spans` flag (default true, so all existing callers are unchanged) to parse_number_string; from_chars_float_advanced parses with it false, attempts the Clinger and Eisel-Lemire fast paths inline, and only re-parses with spans on the two rare slow branches. The re-parse is pushed into a single `fastfloat_noinline` (noinline+cold) helper so the force-inlined hot scanner is emitted once rather than duplicated into the caller (without this the extra inline copies regress some targets, e.g. ARM gcc, by bloating the hot frame and lengthening the loop-carried dependency chain). A runtime flag is used deliberately rather than a template parameter: a template would create a second instantiation of the whole scanner whose icache cost wipes out the gain. Measured (per-parser microbench, median of 5, pinned core), fast_float from_chars <double>/<float>, vs the current tip: - Intel Ice Lake (Xeon 8360Y): +17-19% (gcc), Intel TMA shows backend-bound 26.0% -> 2.2% and retiring 60.3% -> 77.3% on short floats (the eliminated span spill), with -36% pipeline slots. - Intel Cascade Lake (Xeon 6248): +18-22% (gcc), +13-23% (clang). - ARM Neoverse-V2 (Graviton4): +73-196% (gcc), +8-11% (clang) -- the struct spill dominated the gcc hot loop there. Correctness: the full float exhaustive suite (exhaustive32, exhaustive32_64, exhaustive32_midpoint, random64) passes, and a 2^32 sweep is byte-identical to the current tip. Public from_chars / from_chars_advanced / parsed_number_string_t are unchanged.
1 parent 6258cbc commit cb5d9cd

3 files changed

Lines changed: 104 additions & 27 deletions

File tree

include/fast_float/ascii_number.h

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,18 @@ report_parse_error(UC const *p, parse_error error) {
330330

331331
// Assuming that you use no more than 19 digits, this will
332332
// parse an ASCII string.
333+
//
334+
// store_spans is a *runtime* flag (not a template parameter, deliberately: a
335+
// template would create a second instantiation of this whole function and the
336+
// extra icache pressure wipes out the gain). When false, the integer/fraction
337+
// spans (read only by the rare digit_comp slow path) are not materialized, which
338+
// keeps the fat parsed_number_string_t off the hot path. The caller re-parses
339+
// with store_spans=true if the slow path is actually reached.
333340
template <bool basic_json_fmt, typename UC>
334341
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
335342
parse_number_string(UC const *p, UC const *pend,
336-
parse_options_t<UC> options) noexcept {
343+
parse_options_t<UC> options,
344+
bool store_spans = true) noexcept {
337345
chars_format const fmt = detail::adjust_for_feature_macros(options.format);
338346
UC const decimal_point = options.decimal_point;
339347

@@ -402,7 +410,9 @@ parse_number_string(UC const *p, UC const *pend,
402410
}
403411
UC const *const end_of_integer_part = p;
404412
int64_t digit_count = int64_t(end_of_integer_part - start_digits);
405-
answer.integer = span<UC const>(start_digits, size_t(digit_count));
413+
if (store_spans) {
414+
answer.integer = span<UC const>(start_digits, size_t(digit_count));
415+
}
406416
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
407417
// at least 1 digit in integer part, without leading zeros
408418
if (digit_count == 0) {
@@ -429,7 +439,9 @@ parse_number_string(UC const *p, UC const *pend,
429439
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
430440
}
431441
exponent = before - p;
432-
answer.fraction = span<UC const>(before, size_t(p - before));
442+
if (store_spans) {
443+
answer.fraction = span<UC const>(before, size_t(p - before));
444+
}
433445
digit_count -= exponent;
434446
}
435447
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
@@ -514,29 +526,35 @@ parse_number_string(UC const *p, UC const *pend,
514526

515527
if (digit_count > 19) {
516528
answer.too_many_digits = true;
517-
// Let us start again, this time, avoiding overflows.
518-
// We don't need to call if is_integer, since we use the
519-
// pre-tokenized spans from above.
520-
i = 0;
521-
p = answer.integer.ptr;
522-
UC const *int_end = p + answer.integer.len();
523-
uint64_t const minimal_nineteen_digit_integer{1000000000000000000};
524-
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
525-
i = i * 10 + uint64_t(*p - UC('0'));
526-
++p;
527-
}
528-
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
529-
exponent = end_of_integer_part - p + exp_number;
530-
} else { // We have a value with a fractional component.
531-
p = answer.fraction.ptr;
532-
UC const *frac_end = p + answer.fraction.len();
533-
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
529+
// The truncation recompute below reads the integer/fraction spans. When
530+
// store_spans is false we didn't materialize them, so just flag
531+
// too_many_digits; the caller re-parses with store_spans=true to obtain
532+
// the corrected mantissa/exponent before taking the slow path.
533+
if (store_spans) {
534+
// Let us start again, this time, avoiding overflows.
535+
// We don't need to call if is_integer, since we use the
536+
// pre-tokenized spans from above.
537+
i = 0;
538+
p = answer.integer.ptr;
539+
UC const *int_end = p + answer.integer.len();
540+
uint64_t const minimal_nineteen_digit_integer{1000000000000000000};
541+
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
534542
i = i * 10 + uint64_t(*p - UC('0'));
535543
++p;
536544
}
537-
exponent = answer.fraction.ptr - p + exp_number;
545+
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
546+
exponent = end_of_integer_part - p + exp_number;
547+
} else { // We have a value with a fractional component.
548+
p = answer.fraction.ptr;
549+
UC const *frac_end = p + answer.fraction.len();
550+
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
551+
i = i * 10 + uint64_t(*p - UC('0'));
552+
++p;
553+
}
554+
exponent = answer.fraction.ptr - p + exp_number;
555+
}
556+
// We have now corrected both exponent and i, to a truncated value
538557
}
539-
// We have now corrected both exponent and i, to a truncated value
540558
}
541559
}
542560
answer.exponent = exponent;

include/fast_float/float_common.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ using parse_options = parse_options_t<char>;
197197
#define fastfloat_really_inline inline __attribute__((always_inline))
198198
#endif
199199

200+
// Force a function OUT of line and onto the cold path. Used for the rare
201+
// slow-path re-parse so the force-inlined hot scanner is not duplicated into
202+
// the caller (which bloated the hot frame and hurt ILP on some targets).
203+
#ifdef FASTFLOAT_VISUAL_STUDIO
204+
#define fastfloat_noinline __declspec(noinline)
205+
#else
206+
#define fastfloat_noinline __attribute__((noinline, cold))
207+
#endif
208+
200209
#ifndef FASTFLOAT_ASSERT
201210
#define FASTFLOAT_ASSERT(x) \
202211
{ ((void)(x)); }

include/fast_float/parse_number.h

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ from_chars_advanced(parsed_number_string_t<UC> &pns, T &value) noexcept {
289289
return answer;
290290
}
291291

292+
// Cold, out-of-line slow path: re-parse materializing the integer/fraction
293+
// spans the hot no-span parse skipped, then run the full algorithm. Marked
294+
// noinline+cold so the force-inlined spans scanner is emitted ONCE off the hot
295+
// path rather than duplicated into from_chars_float_advanced (which bloated the
296+
// hot frame). from_chars_advanced already handles both the too_many_digits
297+
// disambiguation and the am.power2<0 digit_comp recompute, so both slow branches
298+
// collapse to one helper call.
299+
template <typename T, typename UC>
300+
fastfloat_noinline FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
301+
parse_number_slow_path(UC const *first, UC const *last, T &value,
302+
parse_options_t<UC> options, bool bjf) noexcept {
303+
parsed_number_string_t<UC> pns =
304+
bjf ? parse_number_string<true, UC>(first, last, options, true)
305+
: parse_number_string<false, UC>(first, last, options, true);
306+
return from_chars_advanced(pns, value);
307+
}
308+
292309
template <typename T, typename UC>
293310
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
294311
from_chars_float_advanced(UC const *first, UC const *last, T &value,
@@ -312,10 +329,15 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
312329
answer.ptr = first;
313330
return answer;
314331
}
332+
bool const bjf = uint64_t(fmt & detail::basic_json_fmt) != 0;
333+
334+
// Fast path: parse WITHOUT materializing the integer/fraction spans (read only
335+
// by the rare slow paths). Skipping their stores keeps the fat
336+
// parsed_number_string_t off the hot path. store_spans is a runtime argument,
337+
// so this reuses the single parse_number_string instantiation.
315338
parsed_number_string_t<UC> pns =
316-
uint64_t(fmt & detail::basic_json_fmt)
317-
? parse_number_string<true, UC>(first, last, options)
318-
: parse_number_string<false, UC>(first, last, options);
339+
bjf ? parse_number_string<true, UC>(first, last, options, false)
340+
: parse_number_string<false, UC>(first, last, options, false);
319341
if (!pns.valid) {
320342
if (uint64_t(fmt & chars_format::no_infnan)) {
321343
answer.ec = std::errc::invalid_argument;
@@ -326,8 +348,36 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
326348
}
327349
}
328350

329-
// call overload that takes parsed_number_string_t directly.
330-
return from_chars_advanced(pns, value);
351+
// Slow path A (rare): > 19 significant digits. The no-span parse left the
352+
// mantissa un-truncated and skipped the span-based recompute; the cold helper
353+
// re-parses with spans and runs the full algorithm.
354+
if (pns.too_many_digits) {
355+
return parse_number_slow_path<T, UC>(first, last, value, options, bjf);
356+
}
357+
358+
answer.ec = std::errc(); // be optimistic
359+
answer.ptr = pns.lastmatch;
360+
361+
if (clinger_fast_path_impl(pns.mantissa, pns.exponent, pns.negative, value)) {
362+
return answer;
363+
}
364+
365+
adjusted_mantissa am =
366+
compute_float<binary_format<T>>(pns.exponent, pns.mantissa);
367+
// Slow path B (rare): Eisel-Lemire could not resolve; digit_comp needs the
368+
// integer/fraction spans. Route to the cold helper (clinger there is a
369+
// dead-effect since it already failed here; the cold re-parse + digit_comp via
370+
// from_chars_advanced reproduces this branch).
371+
if (am.power2 < 0) {
372+
return parse_number_slow_path<T, UC>(first, last, value, options, bjf);
373+
}
374+
to_float(pns.negative, am, value);
375+
// Test for over/underflow.
376+
if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) ||
377+
am.power2 == binary_format<T>::infinite_power()) {
378+
answer.ec = std::errc::result_out_of_range;
379+
}
380+
return answer;
331381
}
332382

333383
template <typename T, typename UC, typename>

0 commit comments

Comments
 (0)