Skip to content

fmt of non-decimal radix untangled #143730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

pascaldekloe
Copy link
Contributor

Have the implementation match its decimal counterpart.

  • Digit table instead of conversion functions
  • Correct buffer size per radix
  • Elimination of dead code for negative
  • No trait abstraction for integers

Original Performance

    fmt::write_10ints_bin                                                393.03ns/iter      +/- 1.41
    fmt::write_10ints_hex                                                316.84ns/iter      +/- 1.49
    fmt::write_10ints_oct                                                327.16ns/iter      +/- 0.46

Patched Performance

    fmt::write_10ints_bin                                                392.31ns/iter      +/- 3.05
    fmt::write_10ints_hex                                                302.41ns/iter      +/- 5.48
    fmt::write_10ints_oct                                                322.01ns/iter      +/- 3.82

r? tgross35

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 10, 2025
@pascaldekloe
Copy link
Contributor Author

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@bors
Copy link
Collaborator

bors commented Jul 17, 2025

☔ The latest upstream changes (presumably #144044) made this pull request unmergeable. Please resolve the merge conflicts.

@pascaldekloe
Copy link
Contributor Author

Can you either have a look or reassign @tgross35? Got a followup pending on this one...

@tgross35
Copy link
Contributor

tgross35 commented Jul 18, 2025

I'll reassign for now, but if nobody beats me to it I'll take a look on Monday

r? libs

(leaving myself assigned so it stays on my list)

@rustbot rustbot assigned ibraheemdev and unassigned tgross35 Jul 18, 2025
@tgross35 tgross35 self-assigned this Jul 18, 2025
@tgross35
Copy link
Contributor

By the way; if you are interested, we could use some int (and float) formatting and parsing benchmarks at https://github.com/rust-lang/rustc-perf/blob/2120e3b7b8996e96858b88edefea371679a3d415/collector/runtime-benchmarks/fmt/src/main.rs (I just learned that is possible), which would mean they get run as part of our pretty extensive perf infra rather than you needing to post the results of local runs.

@pascaldekloe
Copy link
Contributor Author

Interesting indeed @tgross35. The specialized benchmarks we have at the moment (such as write_u8_min and write_u64_max) won't scale. Think about how many benchmarks we need for fmt::LowerExp with edge cases on alignment and precision. I am curious to hear what you think of the _10ints_ concept in this patch. The idea is to have a balanced mix of common cases.

Copy link
Contributor

@tgross35 tgross35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few notes, haven't looked at patches 3 & 4 yet

fn to_u64(&self) -> u64;
fn to_u128(&self) -> u128;
}
// Formatting of integers with a non-decimal radix.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, might as well make it a doc comment

Suggested change
// Formatting of integers with a non-decimal radix.
/// Formatting of integers with a non-decimal radix.

Comment on lines 122 to 129
(fmt::$Trait:ident for $T:ident -> $Radix:ident) => {
#[stable(feature = "rust1", since = "1.0.0")]
impl fmt::$Trait for $T {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
$Radix.fmt_int(*self as $U, f)
$Radix.fmt_int(*self, f)
}
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe int_base could do both signed and unsigned rather than having the impls in integer?

    (fmt::$Trait:ident for ($Int:ident, $Uint:ident)  -> $Radix:ident) => {
        #[stable(feature = "rust1", since = "1.0.0")]
        impl fmt::$Trait for $Uint {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                $Radix.fmt_int(*self as $U, f)
                $Radix.fmt_int(*self, f)
            }
        }

        #[stable(feature = "rust1", since = "1.0.0")]
        impl fmt::$Trait for $Int {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::$Trait::fmt(&self.cast_unsigned(), f)
            }
        }
    };

#[stable(feature = "rust1", since = "1.0.0")]
impl fmt::Binary for $Int {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Binary::fmt(&(*self as $Uint), f)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned above but we now have .cast_unsigned() which is slightly cleaner than as $Uint

Comment on lines +166 to +185
#[bench]
fn write_12ints_bin(bh: &mut Bencher) {
bh.iter(|| {
black_box(format!("{:b}", black_box(0_u8)));
black_box(format!("{:b}", black_box(100_i8)));
black_box(format!("{:b}", black_box(-100_i8)));

black_box(format!("{:b}", black_box(0_u32)));
black_box(format!("{:b}", black_box(1000_i32)));
black_box(format!("{:b}", black_box(-1000_i32)));

black_box(format!("{:b}", black_box(0_u64)));
black_box(format!("{:b}", black_box(10000_i64)));
black_box(format!("{:b}", black_box(-10000_i64)));

black_box(format!("{:b}", black_box(0_u128)));
black_box(format!("{:b}", black_box(100000_i128)));
black_box(format!("{:b}", black_box(-100000_i128)));
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to break up benchmarks by integer size because that can be pretty platform-dependent, e.g. u64 might be about the same as u32 on 64-bit platforms, but significantly slower on 32-bit.

Testing a few assorted values seems good though. To get an even balance it may also be good to check:

  • 1 << n for n 0..U::BITS, both signed and unsigned
  • 10.pow(n) for n = 0 up to the max that fits
  • I::MIN, I::MAX, U::MAX

These can be collected outside of bb.iter so the cost of computing values isn't included in the benchmarks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is preexisting but it would also be better to create a String::with_capacity(enough_for_u128), within bb.iter just .clear() and write! into it. Keeps time to alloc/free from skewing benchmark results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants