Skip to content

Commit 5b74275

Browse files
committed
Auto merge of #142294 - GuillaumeGomez:specialize-tostring-on-128-integers, r=tgross35
Use a distinct `ToString` implementation for `u128` and `i128` Part of #135543. Follow-up of #136264. When working on #142098, I realized that `i128` and `u128` could also benefit from a distinct `ToString` implementation so here it. The last commit is just me realizing that I forgot to add the format tests for `usize` and `isize`. Here is the bench comparison: | bench name | last nightly | with this PR | diff | |-|-|-|-| | bench_i128 | 29.25 ns/iter (+/- 0.66) | 17.52 ns/iter (+/- 0.7) | -40.1% | | bench_u128 | 34.06 ns/iter (+/- 0.21) | 16.1 ns/iter (+/- 0.6) | -52.7% | I used this code to test: ```rust #![feature(test)] extern crate test; use test::{Bencher, black_box}; #[inline(always)] fn convert_to_string<T: ToString>(n: T) -> String { n.to_string() } macro_rules! decl_benches { ($($name:ident: $ty:ident,)+) => { $( #[bench] fn $name(c: &mut Bencher) { c.iter(|| convert_to_string(black_box({ let nb: $ty = 20; nb }))); } )+ } } decl_benches! { bench_u128: u128, bench_i128: i128, } ```
2 parents 255aa22 + 9b09948 commit 5b74275

File tree

3 files changed

+110
-94
lines changed

3 files changed

+110
-94
lines changed

library/alloc/src/string.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2877,6 +2877,7 @@ impl_to_string! {
28772877
i32, u32,
28782878
i64, u64,
28792879
isize, usize,
2880+
i128, u128,
28802881
}
28812882

28822883
#[cfg(not(no_global_oom_handling))]

library/alloctests/tests/num.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ int_to_s!(
5252
i32,
5353
test_i64_to_string,
5454
i64,
55+
test_isize_to_string,
56+
isize,
5557
test_i128_to_string,
5658
i128,
5759
);
@@ -64,6 +66,8 @@ uint_to_s!(
6466
u32,
6567
test_u64_to_string,
6668
u64,
69+
test_usize_to_string,
70+
usize,
6771
test_u128_to_string,
6872
u128,
6973
);

library/core/src/fmt/num.rs

Lines changed: 105 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -565,123 +565,134 @@ mod imp {
565565
}
566566
impl_Exp!(i128, u128 as u128 via to_u128 named exp_u128);
567567

568+
const U128_MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1;
569+
568570
#[stable(feature = "rust1", since = "1.0.0")]
569571
impl fmt::Display for u128 {
570572
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
571-
fmt_u128(*self, true, f)
573+
let mut buf = [MaybeUninit::<u8>::uninit(); U128_MAX_DEC_N];
574+
575+
f.pad_integral(true, "", self._fmt(&mut buf))
572576
}
573577
}
574578

575579
#[stable(feature = "rust1", since = "1.0.0")]
576580
impl fmt::Display for i128 {
577581
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578-
fmt_u128(self.unsigned_abs(), *self >= 0, f)
582+
// This is not a typo, we use the maximum number of digits of `u128`, hence why we use
583+
// `U128_MAX_DEC_N`.
584+
let mut buf = [MaybeUninit::<u8>::uninit(); U128_MAX_DEC_N];
585+
586+
let is_nonnegative = *self >= 0;
587+
f.pad_integral(is_nonnegative, "", self.unsigned_abs()._fmt(&mut buf))
579588
}
580589
}
581590

582-
/// Format optimized for u128. Computation of 128 bits is limited by proccessing
583-
/// in batches of 16 decimals at a time.
584-
fn fmt_u128(n: u128, is_nonnegative: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585-
// Optimize common-case zero, which would also need special treatment due to
586-
// its "leading" zero.
587-
if n == 0 {
588-
return f.pad_integral(true, "", "0");
589-
}
591+
impl u128 {
592+
/// Format optimized for u128. Computation of 128 bits is limited by proccessing
593+
/// in batches of 16 decimals at a time.
594+
#[doc(hidden)]
595+
#[unstable(
596+
feature = "fmt_internals",
597+
reason = "specialized method meant to only be used by `SpecToString` implementation",
598+
issue = "none"
599+
)]
600+
pub fn _fmt<'a>(self, buf: &'a mut [MaybeUninit<u8>]) -> &'a str {
601+
// Optimize common-case zero, which would also need special treatment due to
602+
// its "leading" zero.
603+
if self == 0 {
604+
return "0";
605+
}
590606

591-
// U128::MAX has 39 significant-decimals.
592-
const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1;
593-
// Buffer decimals with right alignment.
594-
let mut buf = [MaybeUninit::<u8>::uninit(); MAX_DEC_N];
595-
596-
// Take the 16 least-significant decimals.
597-
let (quot_1e16, mod_1e16) = div_rem_1e16(n);
598-
let (mut remain, mut offset) = if quot_1e16 == 0 {
599-
(mod_1e16, MAX_DEC_N)
600-
} else {
601-
// Write digits at buf[23..39].
602-
enc_16lsd::<{ MAX_DEC_N - 16 }>(&mut buf, mod_1e16);
603-
604-
// Take another 16 decimals.
605-
let (quot2, mod2) = div_rem_1e16(quot_1e16);
606-
if quot2 == 0 {
607-
(mod2, MAX_DEC_N - 16)
607+
// Take the 16 least-significant decimals.
608+
let (quot_1e16, mod_1e16) = div_rem_1e16(self);
609+
let (mut remain, mut offset) = if quot_1e16 == 0 {
610+
(mod_1e16, U128_MAX_DEC_N)
608611
} else {
609-
// Write digits at buf[7..23].
610-
enc_16lsd::<{ MAX_DEC_N - 32 }>(&mut buf, mod2);
611-
// Quot2 has at most 7 decimals remaining after two 1e16 divisions.
612-
(quot2 as u64, MAX_DEC_N - 32)
613-
}
614-
};
612+
// Write digits at buf[23..39].
613+
enc_16lsd::<{ U128_MAX_DEC_N - 16 }>(buf, mod_1e16);
615614

616-
// Format per four digits from the lookup table.
617-
while remain > 999 {
618-
// SAFETY: All of the decimals fit in buf due to MAX_DEC_N
619-
// and the while condition ensures at least 4 more decimals.
620-
unsafe { core::hint::assert_unchecked(offset >= 4) }
621-
// SAFETY: The offset counts down from its initial buf.len()
622-
// without underflow due to the previous precondition.
623-
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
624-
offset -= 4;
615+
// Take another 16 decimals.
616+
let (quot2, mod2) = div_rem_1e16(quot_1e16);
617+
if quot2 == 0 {
618+
(mod2, U128_MAX_DEC_N - 16)
619+
} else {
620+
// Write digits at buf[7..23].
621+
enc_16lsd::<{ U128_MAX_DEC_N - 32 }>(buf, mod2);
622+
// Quot2 has at most 7 decimals remaining after two 1e16 divisions.
623+
(quot2 as u64, U128_MAX_DEC_N - 32)
624+
}
625+
};
625626

626-
// pull two pairs
627-
let quad = remain % 1_00_00;
628-
remain /= 1_00_00;
629-
let pair1 = (quad / 100) as usize;
630-
let pair2 = (quad % 100) as usize;
631-
buf[offset + 0].write(DEC_DIGITS_LUT[pair1 * 2 + 0]);
632-
buf[offset + 1].write(DEC_DIGITS_LUT[pair1 * 2 + 1]);
633-
buf[offset + 2].write(DEC_DIGITS_LUT[pair2 * 2 + 0]);
634-
buf[offset + 3].write(DEC_DIGITS_LUT[pair2 * 2 + 1]);
635-
}
627+
// Format per four digits from the lookup table.
628+
while remain > 999 {
629+
// SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N
630+
// and the while condition ensures at least 4 more decimals.
631+
unsafe { core::hint::assert_unchecked(offset >= 4) }
632+
// SAFETY: The offset counts down from its initial buf.len()
633+
// without underflow due to the previous precondition.
634+
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
635+
offset -= 4;
636+
637+
// pull two pairs
638+
let quad = remain % 1_00_00;
639+
remain /= 1_00_00;
640+
let pair1 = (quad / 100) as usize;
641+
let pair2 = (quad % 100) as usize;
642+
buf[offset + 0].write(DEC_DIGITS_LUT[pair1 * 2 + 0]);
643+
buf[offset + 1].write(DEC_DIGITS_LUT[pair1 * 2 + 1]);
644+
buf[offset + 2].write(DEC_DIGITS_LUT[pair2 * 2 + 0]);
645+
buf[offset + 3].write(DEC_DIGITS_LUT[pair2 * 2 + 1]);
646+
}
636647

637-
// Format per two digits from the lookup table.
638-
if remain > 9 {
639-
// SAFETY: All of the decimals fit in buf due to MAX_DEC_N
640-
// and the if condition ensures at least 2 more decimals.
641-
unsafe { core::hint::assert_unchecked(offset >= 2) }
642-
// SAFETY: The offset counts down from its initial buf.len()
643-
// without underflow due to the previous precondition.
644-
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
645-
offset -= 2;
646-
647-
let pair = (remain % 100) as usize;
648-
remain /= 100;
649-
buf[offset + 0].write(DEC_DIGITS_LUT[pair * 2 + 0]);
650-
buf[offset + 1].write(DEC_DIGITS_LUT[pair * 2 + 1]);
651-
}
648+
// Format per two digits from the lookup table.
649+
if remain > 9 {
650+
// SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N
651+
// and the if condition ensures at least 2 more decimals.
652+
unsafe { core::hint::assert_unchecked(offset >= 2) }
653+
// SAFETY: The offset counts down from its initial buf.len()
654+
// without underflow due to the previous precondition.
655+
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
656+
offset -= 2;
657+
658+
let pair = (remain % 100) as usize;
659+
remain /= 100;
660+
buf[offset + 0].write(DEC_DIGITS_LUT[pair * 2 + 0]);
661+
buf[offset + 1].write(DEC_DIGITS_LUT[pair * 2 + 1]);
662+
}
652663

653-
// Format the last remaining digit, if any.
654-
if remain != 0 {
655-
// SAFETY: All of the decimals fit in buf due to MAX_DEC_N
656-
// and the if condition ensures (at least) 1 more decimals.
657-
unsafe { core::hint::assert_unchecked(offset >= 1) }
658-
// SAFETY: The offset counts down from its initial buf.len()
659-
// without underflow due to the previous precondition.
660-
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
661-
offset -= 1;
662-
663-
// Either the compiler sees that remain < 10, or it prevents
664-
// a boundary check up next.
665-
let last = (remain & 15) as usize;
666-
buf[offset].write(DEC_DIGITS_LUT[last * 2 + 1]);
667-
// not used: remain = 0;
668-
}
664+
// Format the last remaining digit, if any.
665+
if remain != 0 {
666+
// SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N
667+
// and the if condition ensures (at least) 1 more decimals.
668+
unsafe { core::hint::assert_unchecked(offset >= 1) }
669+
// SAFETY: The offset counts down from its initial buf.len()
670+
// without underflow due to the previous precondition.
671+
unsafe { core::hint::assert_unchecked(offset <= buf.len()) }
672+
offset -= 1;
673+
674+
// Either the compiler sees that remain < 10, or it prevents
675+
// a boundary check up next.
676+
let last = (remain & 15) as usize;
677+
buf[offset].write(DEC_DIGITS_LUT[last * 2 + 1]);
678+
// not used: remain = 0;
679+
}
669680

670-
// SAFETY: All buf content since offset is set.
671-
let written = unsafe { buf.get_unchecked(offset..) };
672-
// SAFETY: Writes use ASCII from the lookup table exclusively.
673-
let as_str = unsafe {
674-
str::from_utf8_unchecked(slice::from_raw_parts(
675-
MaybeUninit::slice_as_ptr(written),
676-
written.len(),
677-
))
678-
};
679-
f.pad_integral(is_nonnegative, "", as_str)
681+
// SAFETY: All buf content since offset is set.
682+
let written = unsafe { buf.get_unchecked(offset..) };
683+
// SAFETY: Writes use ASCII from the lookup table exclusively.
684+
unsafe {
685+
str::from_utf8_unchecked(slice::from_raw_parts(
686+
MaybeUninit::slice_as_ptr(written),
687+
written.len(),
688+
))
689+
}
690+
}
680691
}
681692

682693
/// Encodes the 16 least-significant decimals of n into `buf[OFFSET .. OFFSET +
683694
/// 16 ]`.
684-
fn enc_16lsd<const OFFSET: usize>(buf: &mut [MaybeUninit<u8>; 39], n: u64) {
695+
fn enc_16lsd<const OFFSET: usize>(buf: &mut [MaybeUninit<u8>], n: u64) {
685696
// Consume the least-significant decimals from a working copy.
686697
let mut remain = n;
687698

0 commit comments

Comments
 (0)