Split default path into verbatim separator-free scanner

PR #369 gated the digit-separator/prefix feature on a compile-time
has_separator flag and claimed the has_separator==false instantiation
"compiles to exactly the same code as if the feature did not exist".
Instruction-count measurement (benchmark i/f, deterministic +/- 0.0%)
disproved this: routing the default path through the shared, restructured
body cost GCC 14 ~1.5-2.3 i/f on short doubles, a reproducible regression
on mesh/double (+0.77 i/f ASCII, +1.35 UTF-16) even as canada improved.

Fix: the has_separator==false instantiation now delegates to
parse_number_string_nosep, a verbatim copy of the original separator-free
scanner, so the default path's codegen is byte-for-byte the pre-feature
parser. The shared parse_number_string body becomes separator-only (the
dead !has_separator branches are removed). The inaccurate doc comment is
corrected.

Measured vs pre-feature baseline (34164f5), instructions per float:
  canada/f64 ASCII  242.32 -> 238.87   (PR: 240.87)
  canada/f64 UTF16  251.05 -> 241.81   (PR: 245.79)
  mesh/f64   ASCII  107.69 -> 107.57   (PR: 108.46, +0.77 regression)
  mesh/f64   UTF16  110.10 -> 109.74   (PR: 111.45, +1.35 regression)
All eight fastfloat rows now match or beat both the baseline and the PR;
the mesh/double regression is eliminated. All 14 ctest suites pass.
This commit is contained in:
Daniel Lemire 2026-07-13 11:09:17 -04:00
parent 8428fd7c49
commit f1ea4556f9

View File

@ -328,6 +328,233 @@ report_parse_error(UC const *p, parse_error error) {
return answer;
}
// Verbatim copy of the original separator-free scanner. parse_number_string
// delegates here for the default (has_separator == false) instantiation so that
// path's code generation is identical to the pre-feature parser.
template <bool basic_json_fmt, typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
parse_number_string_nosep(UC const *p, UC const *pend, parse_options_t<UC> options,
bool store_spans = true) noexcept {
chars_format const fmt = detail::adjust_for_feature_macros(options.format);
UC const decimal_point = options.decimal_point;
parsed_number_string_t<UC> answer;
answer.valid = false;
answer.too_many_digits = false;
// assume p < pend, so dereference without checks;
answer.negative = (*p == UC('-'));
// C++17 20.19.3.(7.1) explicitly forbids '+' sign here
if ((*p == UC('-')) || (uint64_t(fmt & chars_format::allow_leading_plus) &&
!basic_json_fmt && *p == UC('+'))) {
++p;
if (p == pend) {
return report_parse_error<UC>(
p, parse_error::missing_integer_or_dot_after_sign);
}
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
if (!is_integer(*p)) { // a sign must be followed by an integer
return report_parse_error<UC>(p,
parse_error::missing_integer_after_sign);
}
}
else {
if (!is_integer(*p) &&
(*p !=
decimal_point)) { // a sign must be followed by an integer or the dot
return report_parse_error<UC>(
p, parse_error::missing_integer_or_dot_after_sign);
}
}
}
UC const *const start_digits = p;
uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
// Straight-line unroll of the integer-part scan: most integer parts are
// 1-5 digits, so peeling the first iterations eliminates the loop back-edge
// for the common case. Semantics are identical to the original `while` loop:
// i = 10*i + digit, advancing p.
if ((p != pend) && is_integer(*p)) {
i = uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
while ((p != pend) && is_integer(*p)) {
// a multiplication by 10 is cheaper than an arbitrary integer
// multiplication
i = 10 * i +
uint64_t(*p - UC('0')); // might overflow, handled later
++p;
}
}
}
}
}
}
UC const *const end_of_integer_part = p;
int64_t digit_count = int64_t(end_of_integer_part - start_digits);
if (store_spans) {
answer.integer = span<UC const>(start_digits, size_t(digit_count));
}
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
// at least 1 digit in integer part, without leading zeros
if (digit_count == 0) {
return report_parse_error<UC>(p, parse_error::no_digits_in_integer_part);
}
if ((start_digits[0] == UC('0') && digit_count > 1)) {
return report_parse_error<UC>(start_digits,
parse_error::leading_zeros_in_integer_part);
}
}
int64_t exponent = 0;
bool const has_decimal_point = (p != pend) && (*p == decimal_point);
if (has_decimal_point) {
++p;
UC const *before = p;
// can occur at most twice without overflowing, but let it occur more, since
// for integers with many digits, digit parsing is the primary bottleneck.
loop_parse_if_eight_digits(p, pend, i);
while ((p != pend) && is_integer(*p)) {
uint8_t digit = uint8_t(*p - UC('0'));
++p;
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
}
exponent = before - p;
if (store_spans) {
answer.fraction = span<UC const>(before, size_t(p - before));
}
digit_count -= exponent;
}
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
// at least 1 digit in fractional part
if (has_decimal_point && exponent == 0) {
return report_parse_error<UC>(p,
parse_error::no_digits_in_fractional_part);
}
}
else if (digit_count == 0) { // we must have encountered at least one integer!
return report_parse_error<UC>(p, parse_error::no_digits_in_mantissa);
}
int64_t exp_number = 0; // explicit exponential part
if ((uint64_t(fmt & chars_format::scientific) && (p != pend) &&
((UC('e') == *p) || (UC('E') == *p))) ||
(uint64_t(fmt & detail::basic_fortran_fmt) && (p != pend) &&
((UC('+') == *p) || (UC('-') == *p) || (UC('d') == *p) ||
(UC('D') == *p)))) {
UC const *location_of_e = p;
if ((UC('e') == *p) || (UC('E') == *p) || (UC('d') == *p) ||
(UC('D') == *p)) {
++p;
}
bool neg_exp = false;
if ((p != pend) && (UC('-') == *p)) {
neg_exp = true;
++p;
} else if ((p != pend) &&
(UC('+') ==
*p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1)
++p;
}
if ((p == pend) || !is_integer(*p)) {
if (!uint64_t(fmt & chars_format::fixed)) {
// The exponential part is invalid for scientific notation, so it must
// be a trailing token for fixed notation. However, fixed notation is
// disabled, so report a scientific notation error.
return report_parse_error<UC>(p, parse_error::missing_exponential_part);
}
// Otherwise, we will be ignoring the 'e'.
p = location_of_e;
} else {
while ((p != pend) && is_integer(*p)) {
uint8_t digit = uint8_t(*p - UC('0'));
if (exp_number < 0x10000000) {
exp_number = 10 * exp_number + digit;
}
++p;
}
if (neg_exp) {
exp_number = -exp_number;
}
exponent += exp_number;
}
} else {
// If it scientific and not fixed, we have to bail out.
if (uint64_t(fmt & chars_format::scientific) &&
!uint64_t(fmt & chars_format::fixed)) {
return report_parse_error<UC>(p, parse_error::missing_exponential_part);
}
}
answer.lastmatch = p;
answer.valid = true;
// If we frequently had to deal with long strings of digits,
// we could extend our code by using a 128-bit integer instead
// of a 64-bit integer. However, this is uncommon.
//
// We can deal with up to 19 digits.
if (digit_count > 19) { // this is uncommon
// It is possible that the integer had an overflow.
// We have to handle the case where we have 0.0000somenumber.
// We need to be mindful of the case where we only have zeroes...
// E.g., 0.000000000...000.
UC const *start = start_digits;
while ((start != pend) && (*start == UC('0') || *start == decimal_point)) {
if (*start == UC('0')) {
digit_count--;
}
start++;
}
if (digit_count > 19) {
answer.too_many_digits = true;
// The truncation recompute below reads the integer/fraction spans. When
// store_spans is false we didn't materialize them, so just flag
// too_many_digits; the caller re-parses with store_spans=true to obtain
// the corrected mantissa/exponent before taking the slow path.
if (store_spans) {
// Let us start again, this time, avoiding overflows.
// We don't need to call if is_integer, since we use the
// pre-tokenized spans from above.
i = 0;
p = answer.integer.ptr;
UC const *int_end = p + answer.integer.len();
uint64_t const minimal_nineteen_digit_integer{1000000000000000000};
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
i = i * 10 + uint64_t(*p - UC('0'));
++p;
}
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
exponent = end_of_integer_part - p + exp_number;
} else { // We have a value with a fractional component.
p = answer.fraction.ptr;
UC const *frac_end = p + answer.fraction.len();
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
i = i * 10 + uint64_t(*p - UC('0'));
++p;
}
exponent = answer.fraction.ptr - p + exp_number;
}
// We have now corrected both exponent and i, to a truncated value
}
}
}
answer.exponent = exponent;
answer.mantissa = i;
return answer;
}
// Assuming that you use no more than 19 digits, this will
// parse an ASCII string.
//
@ -338,23 +565,27 @@ report_parse_error(UC const *p, parse_error error) {
// which keeps the fat parsed_number_string_t off the hot path. The caller
// re-parses with store_spans=true if the slow path is actually reached.
//
// has_separator is a *compile-time* flag (the opposite choice from store_spans,
// and deliberately so): the separator-aware code paths are an opt-in feature
// that the vast majority of callers never enable. Gating them on a template
// parameter means the has_separator==false instantiation -- the default that
// everybody uses -- compiles to exactly the same code as if the feature did not
// exist: no separator comparison ever enters a digit loop, and the SIMD
// eight-digit fast path stays intact. The has_separator==true instantiation is
// cold code that default callers never execute. See parse_number_string_options
// for the runtime->compile-time dispatch.
// has_separator is a *compile-time* flag: the separator-aware code paths are an
// opt-in feature that the vast majority of callers never enable. When
// has_separator == false this function simply delegates to
// parse_number_string_nosep, a verbatim copy of the original separator-free
// scanner, so the default instantiation is byte-for-byte the pre-feature parser
// (the SIMD eight-digit fast path and unrolled integer scan are untouched). The
// has_separator == true instantiation below is cold code that default callers
// never execute. See parse_number_string_options for the runtime->compile-time
// dispatch.
template <bool basic_json_fmt, bool has_separator, typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
bool store_spans = true) noexcept {
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
return parse_number_string_nosep<basic_json_fmt, UC>(p, pend, options,
store_spans);
}
chars_format const fmt = detail::adjust_for_feature_macros(options.format);
UC const decimal_point = options.decimal_point;
UC const separator = options.digit_separator;
(void)separator; // unused when has_separator == false
parsed_number_string_t<UC> answer;
answer.valid = false;
@ -391,43 +622,7 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
// Points at the first actual digit (== start_digits when no separator
// precedes it). Used only by the basic_json leading-zero check.
UC const *first_digit_ptr = start_digits;
(void)first_digit_ptr; // only read in the basic_json_fmt path
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
// Straight-line unroll of the integer-part scan: most integer parts are
// 1-5 digits, so peeling the first iterations eliminates the loop back-edge
// for the common case. Semantics are identical to the original `while`
// loop: i = 10*i + digit, advancing p.
if ((p != pend) && is_integer(*p)) {
i = uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
i = 10 * i + uint64_t(*p - UC('0'));
++p;
while ((p != pend) && is_integer(*p)) {
// a multiplication by 10 is cheaper than an arbitrary integer
// multiplication
i = 10 * i +
uint64_t(*p - UC('0')); // might overflow, handled later
++p;
}
}
}
}
}
}
digit_count = int64_t(p - start_digits);
}
else {
// Separator-aware scan: a configured digit separator (e.g. '\'') may appear
// between digits. It is skipped and does not contribute to the value or the
// digit count, but it is retained in the integer span below so the overflow
@ -447,12 +642,10 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
++p;
++digit_count;
}
}
UC const *const end_of_integer_part = p;
if (store_spans) {
// The span keeps the raw characters (separators included) so the overflow
// re-scan below can re-tokenize correctly; for has_separator == false the
// length equals digit_count.
// re-scan below can re-tokenize correctly.
answer.integer = span<UC const>(start_digits,
size_t(end_of_integer_part - start_digits));
}
@ -473,20 +666,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
++p;
UC const *before = p;
int64_t fractional_digit_count = 0;
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
// can occur at most twice without overflowing, but let it occur more,
// since for integers with many digits, digit parsing is the primary
// bottleneck.
loop_parse_if_eight_digits(p, pend, i);
while ((p != pend) && is_integer(*p)) {
uint8_t digit = uint8_t(*p - UC('0'));
++p;
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
}
fractional_digit_count = int64_t(p - before);
}
else {
while (p != pend) {
if (*p == separator) {
++p;
@ -500,7 +679,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
++fractional_digit_count;
}
}
exponent = -fractional_digit_count;
if (store_spans) {
answer.fraction = span<UC const>(before, size_t(p - before));
@ -547,16 +725,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
// Otherwise, we will be ignoring the 'e'.
p = location_of_e;
} else {
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
while ((p != pend) && is_integer(*p)) {
uint8_t digit = uint8_t(*p - UC('0'));
if (exp_number < 0x10000000) {
exp_number = 10 * exp_number + digit;
}
++p;
}
}
else {
while (p != pend) {
if (*p == separator) {
++p;
@ -571,7 +739,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
}
++p;
}
}
if (neg_exp) {
exp_number = -exp_number;
}
@ -596,12 +763,10 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
// It is possible that the integer had an overflow.
// We have to handle the case where we have 0.0000somenumber.
// We need to be mindful of the case where we only have zeroes...
// E.g., 0.000000000...000. The `has_separator &&` guard below is a
// compile-time constant, so this loop is identical to the original when the
// feature is disabled.
// E.g., 0.000000000...000.
UC const *start = start_digits;
while ((start != pend) && (*start == UC('0') || *start == decimal_point ||
(has_separator && *start == separator))) {
*start == separator)) {
if (*start == UC('0')) {
digit_count--;
}
@ -622,24 +787,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
p = answer.integer.ptr;
UC const *int_end = p + answer.integer.len();
uint64_t const minimal_nineteen_digit_integer{1000000000000000000};
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
i = i * 10 + uint64_t(*p - UC('0'));
++p;
}
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
exponent = end_of_integer_part - p + exp_number;
} else { // We have a value with a fractional component.
p = answer.fraction.ptr;
UC const *frac_end = p + answer.fraction.len();
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
i = i * 10 + uint64_t(*p - UC('0'));
++p;
}
exponent = answer.fraction.ptr - p + exp_number;
}
}
else {
// Separator-aware re-scan: separators are skipped and excluded from
// the digit counts that determine the exponent.
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
@ -676,7 +823,6 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
}
exponent = exp_number - fraction_digits_consumed;
}
}
// We have now corrected both exponent and i, to a truncated value
}
}