Skip to content

Commit baf578c

Browse files
committed
Auto merge of #122770 - iximeow:ixi/int-formatting-optimization, r=<try>
improve codegen of fmt_num to delete unreachable panic it seems LLVM doesn't realize that `curr` is always decremented at least once in either loop formatting characters of the input string by their appropriate radix, and so the later `&buf[curr..]` generates a check for out-of-bounds access and panic. this is unreachable in reality as even for `x == T::zero()` we'll produce at least the character `Self::digit(T::zero())`, yielding at least one character output, and `curr` will always be at least one below `buf.len()`. adjust `fmt_int` to make this fact more obvious to the compiler, which fortunately (or unfortunately) results in a measurable performance improvement for workloads heavy on formatting integers. in the program i'd noticed this in, you can see the `cmp $0x80,%rdi; ja 7c` here, which branches to a slice index fail helper: <img width="660" alt="before" src="https://github.com/rust-lang/rust/assets/4615790/ac482d54-21f8-494b-9c83-4beadc3ca0ef"> where after this change the function is broadly similar, but smaller, with one fewer registers updated in each pass through the loop in addition the never-taken `cmp/ja` being gone: <img width="646" alt="after" src="https://github.com/rust-lang/rust/assets/4615790/1bee1d76-b674-43ec-9b21-4587364563aa"> this represents a ~2-3% difference in runtime in my [admittedly comically i32-formatting-bound](https://github.com/athre0z/disas-bench/blob/master/bench/yaxpeax/src/main.rs#L58-L67) use case (printing x86 instructions, including i32 displacements and immediates) as measured on a ryzen 9 3950x. the impact on `<impl LowerHex for i8>::fmt` is both more dramatic and less impactful: it continues to have a loop that is evaluated at most twice, though the compiler doesn't know that to unroll it. the generated code there is identical to the impl for `i32`. there, the smaller loop body has less effect on runtime, and removing the never-taken slice bounds check is offset by whatever address recalculation is happening with the `lea/add/neg` at the end of the loop. it behaves about the same before and after. --- i went through a few tries at getting llvm to understand the bounds check isn't necessary, but i should mention the _best_ i'd seen here was actually from the existing `fmt_int` with a diff like ```diff if x == zero { // No more digits left to accumulate. break; }; } } + + if curr >= buf.len() { + unsafe { core::hint::unreachable_unchecked(); } + } let buf = &buf[curr..]; ``` posting a random PR to `rust-lang/rust` to do that without a really really compelling reason seemed a bit absurd, so i tried to work that into something that seems more palatable at a glance. but if you're interested, that certainly produced better (x86_64) code through LLVM. in that case with `buf.iter_mut().rev()` as the iterator, `<impl LowerHex for i8>::fmt` actually unrolls into something like ``` put_char(x & 0xf); let mut len = 1; if x > 0xf { put_char((x >> 4) & 0xf); len = 2; } pad_integral(buf[buf.len() - len..]); ``` it's pretty cool! `<impl LowerHex for i32>::fmt` also was slightly better. that all resulted in closer to an 6% difference in my use case. --- i have not looked at formatters other than LowerHex/UpperHex with this change, though i'd be a bit shocked if any were _worse_. (i have absolutely _no_ idea how you'd regression test this, but that might be just my not knowing what the right tool for that would be in rust-lang/rust. i'm of half a mind that this is small and fiddly enough to not be worth landing lest it quietly regress in the future anyway. but i didn't want to discard the idea without at least offering it upstream here)
2 parents a128516 + e7fbf72 commit baf578c

File tree

1 file changed

+4
-4
lines changed

1 file changed

+4
-4
lines changed

library/core/src/fmt/num.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,23 @@ unsafe trait GenericRadix: Sized {
7979
if is_nonnegative {
8080
// Accumulate each digit of the number from the least significant
8181
// to the most significant figure.
82-
for byte in buf.iter_mut().rev() {
82+
loop {
8383
let n = x % base; // Get the current place value.
8484
x = x / base; // Deaccumulate the number.
85-
byte.write(Self::digit(n.to_u8())); // Store the digit in the buffer.
8685
curr -= 1;
86+
buf[curr].write(Self::digit(n.to_u8())); // Store the digit in the buffer.
8787
if x == zero {
8888
// No more digits left to accumulate.
8989
break;
9090
};
9191
}
9292
} else {
9393
// Do the same as above, but accounting for two's complement.
94-
for byte in buf.iter_mut().rev() {
94+
loop {
9595
let n = zero - (x % base); // Get the current place value.
9696
x = x / base; // Deaccumulate the number.
97-
byte.write(Self::digit(n.to_u8())); // Store the digit in the buffer.
9897
curr -= 1;
98+
buf[curr].write(Self::digit(n.to_u8())); // Store the digit in the buffer.
9999
if x == zero {
100100
// No more digits left to accumulate.
101101
break;

0 commit comments

Comments
 (0)