From 228c8554973c550a7f501dfa8ffdb0ae96a78745 Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Mon, 18 May 2026 08:44:42 +0100 Subject: [PATCH 1/3] feat: improve integer parsing performance Signed-off-by: Giles Cope --- library/core/src/num/mod.rs | 63 +++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/library/core/src/num/mod.rs b/library/core/src/num/mod.rs index fdd4831336179..cb4251c24d070 100644 --- a/library/core/src/num/mod.rs +++ b/library/core/src/num/mod.rs @@ -1518,6 +1518,29 @@ pub enum FpCategory { Normal, } +/// Largest `k` such that `radix.pow(k) <= 2.pow(bits)` — the largest digit +/// count for which the unchecked `result = result * radix + d` accumulator is +/// guaranteed not to overflow an integer of magnitude `2.pow(bits) - 1`. +/// `radix` must be in `2..=36`. +/// +/// Implemented as a macro because a `const fn` defeats cross-crate inlining. +macro_rules! max_safe_digits { + ($bits:expr, $radix:expr) => {{ + let bits: u32 = $bits; + let radix_u128 = ($radix) as u128; + if bits < 128 { + (1u128 << bits).ilog(radix_u128) + } else { + // 2.pow(128) doesn't fit u128. `u128::MAX.ilog(radix)` agrees with + // the true answer except when `radix` exactly tiles 128 bits + // (power of two dividing 128), where it undercounts by 1. + let base = u128::MAX.ilog(radix_u128); + let exact_tile = ($radix).is_power_of_two() && 128 % ($radix).ilog2() == 0; + base + exact_tile as u32 + } + }}; +} + /// Determines if a string of text of that length of that radix could be guaranteed to be /// stored in the given type T. /// Note that if the radix is known to the compiler, it is just the check of digits.len that @@ -1526,7 +1549,8 @@ pub enum FpCategory { #[inline(always)] #[unstable(issue = "none", feature = "std_internals")] pub const fn can_not_overflow(radix: u32, is_signed_ty: bool, digits: &[u8]) -> bool { - radix <= 16 && digits.len() <= size_of::() * 2 - is_signed_ty as usize + let bits = (size_of::() * 8 - is_signed_ty as usize) as u32; + digits.len() <= max_safe_digits!(bits, radix) as usize } #[cfg_attr(not(panic = "immediate-abort"), inline(never))] @@ -1747,7 +1771,42 @@ macro_rules! from_str_int_impl { }; } - if can_not_overflow::<$int_ty>(radix, is_signed_ty, digits) { + // Capture the original-input overflow decision *before* the + // u64 fast-prefix consumes any digits — consuming a safe + // prefix only shrinks the remaining-digit count, so an + // originally-safe input stays safe. + let no_overflow = can_not_overflow::<$int_ty>(radix, is_signed_ty, digits); + + // u64 fast-prefix for types wider than `u64` (i.e. `i128`/`u128`): + // 128-bit multiply/add are several times slower than 64-bit on + // 64-bit hardware, so parse as many leading digits as fit in + // `u64`, then promote and let the main loop finish the rest. + if size_of::<$int_ty>() > size_of::() && !digits.is_empty() { + let prefix_max = max_safe_digits!(64u32, radix) as usize; + // `usize::min` is `const fn` but gated by an unstable + // feature this function can't depend on yet. + let take = if digits.len() < prefix_max { digits.len() } else { prefix_max }; + let (mut head, tail) = digits.split_at(take); + let mut r64: u64 = 0; + while let [c, rest @ ..] = head { + let d = unwrap_or_PIE!((*c as char).to_digit(radix), InvalidDigit); + // `head.len() <= prefix_max` keeps `r64` within `u64`. + r64 = r64 * radix as u64 + d as u64; + head = rest; + } + // For unsigned `$int_ty`, `is_positive` is always `true` + // (the `-` arm of the sign match requires `is_signed_ty`), + // so the negative branch is dead and gets eliminated. + // For `i128`, `r64 < 2^64` so the negation cannot overflow. + result = if is_positive { + r64 as $int_ty + } else { + (r64 as $int_ty).wrapping_neg() + }; + digits = tail; + } + + if no_overflow { // If the len of the str is short compared to the range of the type // we are parsing into, then we can be certain that an overflow will not occur. // This bound is when `radix.pow(digits.len()) - 1 <= T::MAX` but the condition From 8d63c50c501e5f632de43ebe95e94b0257744029 Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Mon, 18 May 2026 11:34:57 +0100 Subject: [PATCH 2/3] fix: needs inline always hint Signed-off-by: Giles Cope --- library/core/src/num/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/num/mod.rs b/library/core/src/num/mod.rs index cb4251c24d070..176d330873f4e 100644 --- a/library/core/src/num/mod.rs +++ b/library/core/src/num/mod.rs @@ -1735,7 +1735,9 @@ macro_rules! from_str_int_impl { #[doc = concat!("assert!(", stringify!($int_ty), "::from_ascii_radix(b\"1 \", 10).is_err());")] /// ``` #[unstable(feature = "int_from_ascii", issue = "134821")] - #[inline] + // `inline(always)` so the body's `radix`-dependent `ilog` bound + // and fast-prefix size const-fold at literal-radix call sites. + #[inline(always)] pub const fn from_ascii_radix(src: &[u8], radix: u32) -> Result<$int_ty, ParseIntError> { use self::IntErrorKind::*; use self::ParseIntError as PIE; From 9eab6e1b66c23df3a4c1af0b0f2a86975b80e5f1 Mon Sep 17 00:00:00 2001 From: Giles Cope Date: Mon, 18 May 2026 13:29:35 +0100 Subject: [PATCH 3/3] fix: 2 and 4 should now be included Signed-off-by: Giles Cope --- library/coretests/tests/num/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/coretests/tests/num/mod.rs b/library/coretests/tests/num/mod.rs index a82ac6fcd45f0..cac025db8ee68 100644 --- a/library/coretests/tests/num/mod.rs +++ b/library/coretests/tests/num/mod.rs @@ -213,8 +213,11 @@ fn test_can_not_overflow() { for base in 2..=36 { let num = ::MAX; let max_len_string = format_radix(num, base as u128); - // base 16 fits perfectly for u128 and won't overflow: - assert_eq!(can_overflow::(base, &max_len_string), base != 16); + // Powers of two that divide 128 (2, 4, 16) produce a max-length + // string equal to u128::MAX itself, so the precise overflow check + // sees it as safe. + let fits_exactly = matches!(base, 2 | 4 | 16); + assert_eq!(can_overflow::(base, &max_len_string), !fits_exactly); } }