perf(parser): use a stack buffer for float separators (#28435)

Avoid temporary heap strings for floats such as `1_000.5` with a 64-byte
stack buffer, keeping the existing fallback for longer literals.

Local arm64 benchmarks: 26–57% less parsing time on short
separated-float fixtures; ordinary corpus +0.35% without tokens / −0.25%
with tokens. Integer-only token controls are ~4% slower.

Parser/Node checks pass, conformance is unchanged, and 58,500
differential comparisons match.

AI assistance: Codex.
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 11:35:06 +08:00
1 parent 6e35e3c268
commit 5238839466
2 files changed
+48 -4

No files matched your search

+45 -1
View File
@@ -44,11 +44,35 @@ pub fn parse_int(s: &str, kind: Kind, has_sep: bool) -> Result<f64, &'static str
}
pub fn parse_float(s: &str, has_sep: bool) -> Result<f64, &'static str> {
let s = if has_sep { s.cow_replace('_', "") } else { Cow::Borrowed(s) };
if has_sep {
return parse_float_with_underscores(s);
}
debug_assert!(!s.contains('_'));
s.parse::<f64>().map_err(|_| "invalid float")
}
#[cold]
fn parse_float_with_underscores(s: &str) -> Result<f64, &'static str> {
// Common numeric literals fit in a small scratch buffer. Keep the heap fallback
// for arbitrarily long literals without allocating for every separated literal.
const CAPACITY: usize = 64;
if s.len() > CAPACITY {
return s.cow_replace('_', "").parse::<f64>().map_err(|_| "invalid float");
}
let mut buffer = [0; CAPACITY];
let mut len = 0;
for byte in s.bytes() {
if byte != b'_' {
buffer[len] = byte;
len += 1;
}
}
// Removing an ASCII character preserves UTF-8.
let s = std::str::from_utf8(&buffer[..len]).unwrap();
s.parse::<f64>().map_err(|_| "invalid float")
}
// ==================================== DECIMAL ====================================
/// b'0' is 0x30 and b'9' is 0x39.
@@ -611,6 +635,26 @@ mod test {
assert_all_floats_eq(cases, false);
}
#[test]
fn test_decimal_separators_rounding_and_buffer_boundary() {
for (literal, bits) in [
("1.0000000000000001110223024625156540423631668090820312_5", 0x3ff0_0000_0000_0000),
("1.0000000000000001110223024625156540423631668090820312_6", 0x3ff0_0000_0000_0001),
("1.797_693_134_862_315_7e308", 0x7fef_ffff_ffff_ffff),
("1.797_693_134_862_315_9e308", 0x7ff0_0000_0000_0000),
("4.940_656_458_412_465_4e-324", 1),
("2.470_328_229_206_232_7e-324", 0),
("2.470_328_229_206_232_8e-324", 1),
] {
assert_eq!(parse_float(literal, true).unwrap().to_bits(), bits, "{literal}");
}
for len in [63, 64, 65, 128] {
let literal = format!("1.5_{}", "0".repeat(len - 4));
assert_eq!(parse_float(&literal, true), Ok(1.5), "{literal}");
}
assert_eq!(parse_float("1_µ.5", true), Err("invalid float"));
}
#[test]
fn test_parse_int_no_sep() {
let decimal: Vec<(&str, i64)> = vec![
@@ -66,9 +66,9 @@ binder.ts:
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
sys allocs: 183
sys reallocs: 160
sys deallocs: 183
sys allocs: 182
sys reallocs: 159
sys deallocs: 182
sys alloc bytes: 34053 # 34.05 kB
sys peak growth: 3892 # 3.89 kB
arena allocs: 135741