Skip to content

Conversation

@jhpratt
Copy link
Member

@jhpratt jhpratt commented Nov 2, 2025

In some places, the standard library calls out to a standalone method (core::panicking::panic) when panicking to avoid unnecessary use of the formatting machinery. This PR attempts to optimize panic! calls directly and for everyone, rather than requiring manual effort to call the method.

The current approach is to intercept panic! and unreachable! when it's going thru edition resolution. If any only if the macro call consists of exactly one string literal and there are no formatting placeholders (determined laxly by searching for braces in the string), then we pass the message into the method call. In the case of unreachable!, we need to prepend the message prefix, necessitating unescaping as that info isn't saved at this point of the compilation process.

As with any optimization, I think this will work, but a perf run will be necessary.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Nov 2, 2025
@jhpratt jhpratt added A-panic Area: Panicking machinery C-optimization Category: An issue highlighting optimization opportunities or PRs implementing such labels Nov 2, 2025
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rustbot rustbot added the T-clippy Relevant to the Clippy team. label Nov 2, 2025
@jhpratt

This comment was marked as resolved.

@rust-timer

This comment was marked as resolved.

rust-bors bot added a commit that referenced this pull request Nov 2, 2025
Optimize `panic!(<str lit>)` into method call
@rust-bors

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Nov 2, 2025
@rust-bors

This comment was marked as resolved.

@rust-timer

This comment was marked as resolved.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (d1479e2): comparison URL.

Overall result: ❌✅ regressions and improvements - please read the text below

Benchmarking this pull request means it may be perf-sensitive – we'll automatically label it not fit for rolling up. You can override this, but we strongly advise not to, due to possible changes in compiler perf.

Next Steps: If you can justify the regressions found in this try perf run, please do so in sufficient writing along with @rustbot label: +perf-regression-triaged. If not, please fix the regressions and do another perf run. If its results are neutral or positive, the label will be automatically removed.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
3.0% [3.0%, 3.0%] 1
Regressions ❌
(secondary)
0.1% [0.1%, 0.1%] 2
Improvements ✅
(primary)
-0.3% [-0.9%, -0.2%] 22
Improvements ✅
(secondary)
-0.4% [-2.8%, -0.0%] 11
All ❌✅ (primary) -0.2% [-0.9%, 3.0%] 23

Max RSS (memory usage)

Results (primary -0.8%, secondary -3.1%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
3.1% [3.0%, 3.1%] 2
Regressions ❌
(secondary)
3.0% [3.0%, 3.0%] 1
Improvements ✅
(primary)
-2.1% [-3.9%, -0.8%] 6
Improvements ✅
(secondary)
-3.6% [-5.6%, -1.4%] 12
All ❌✅ (primary) -0.8% [-3.9%, 3.1%] 8

Cycles

Results (primary 3.0%, secondary 1.0%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
3.0% [3.0%, 3.0%] 1
Regressions ❌
(secondary)
4.5% [2.3%, 9.1%] 5
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-4.7% [-6.2%, -2.5%] 3
All ❌✅ (primary) 3.0% [3.0%, 3.0%] 1

Binary size

Results (primary -0.2%, secondary -0.3%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.4% [0.0%, 1.0%] 3
Regressions ❌
(secondary)
0.0% [0.0%, 0.1%] 2
Improvements ✅
(primary)
-0.2% [-0.9%, -0.0%] 102
Improvements ✅
(secondary)
-0.3% [-2.7%, -0.0%] 59
All ❌✅ (primary) -0.2% [-0.9%, 1.0%] 105

Bootstrap: 474.434s -> 474.79s (0.08%)
Artifact size: 390.87 MiB -> 388.73 MiB (-0.55%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels Nov 2, 2025
@jhpratt
Copy link
Member Author

jhpratt commented Nov 2, 2025

Awesome, it actually worked!

Marking as ready for review, though I'm going to add some doc changes and misc. cleanups in nearby code. The existing changes won't be altered.

@jhpratt jhpratt marked this pull request as ready for review November 2, 2025 20:54
@rustbot
Copy link
Collaborator

rustbot commented Nov 2, 2025

The Miri subtree was changed

cc @rust-lang/miri

These commits modify the Cargo.lock file. Unintentional changes to Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Nov 2, 2025
@rustbot
Copy link
Collaborator

rustbot commented Nov 2, 2025

r? @lcnr

rustbot has assigned @lcnr.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@jhpratt
Copy link
Member Author

jhpratt commented Nov 2, 2025

re. the pings: miri is simply blessing a test since the backtrace changed (expected), and clippy is updating the MSRV lint to handle core, alloc, and std instead of just core. This is necessary as a test would fail without std being included.

This ignores `core`, `alloc`, and `std` rather than just `core`. The
same reasoning applies.
@jhpratt
Copy link
Member Author

jhpratt commented Nov 3, 2025

@m-ou-se as the formatting guru…Would it be feasible to leverage the internals of format_args! to inline literals and other format_args! calls, using fmt::Arguments::as_str to get the final string out? If it's feasible, it would provide more opportunity for optimization, though I'd need to re-benchmark to check as there would be more work being done.

Copy link
Contributor

@lcnr lcnr left a comment

Choose a reason for hiding this comment

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

am not too familiar with this code, but it looks good to me. cc @rust-lang/libs on whether this is generally something you want

View changes since this review

&& let TokenKind::Literal(lit) = &token.kind
&& let Lit { kind: LitKind::Str | LitKind::StrRaw(_), symbol, .. } = lit
&& let msg = symbol.as_str()
&& !msg.contains(|c| c == '{' || c == '}')
Copy link
Member

Choose a reason for hiding this comment

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

This is very much a hack. It would be much better to use rustc_parse_format here.

In particular since I think your version would not optimize panic!("foo {{ bar }}"); since it doesn't know about the escaping.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm aware. I tried to use that crate but couldn't figure out how to get it to work despite multiple approaches.

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 this should work:

    if tts.len() == 1
        && let Some(TokenTree::Token(token, _)) = tts.get(0)
        && let TokenKind::Literal(lit) = &token.kind
        && let Lit { kind: litkind, symbol, .. } = lit
        && let Some(style) = match litkind {
            LitKind::Str => Some(None),
            LitKind::StrRaw(n) => Some(Some(*n as usize)),
            _ => None,
        }
        && let mut parser = rustc_parse_format::Parser::new(
            symbol.as_str(),
            style,
            None,
            true,
            rustc_parse_format::ParseMode::Format,
        )
        && let Some(rustc_parse_format::Piece::Lit(msg)) = parser.next()
        && let None = parser.next()
    {
        let msg = match mac {
            InnerCall::Panic2015 | InnerCall::Panic2021 => cx.expr(sp, ExprKind::Lit(*lit)),
            InnerCall::Unreachable2015 | InnerCall::Unreachable2021 => cx.expr_str(
                sp,
                Symbol::intern(&format!("internal error: entered unreachable code: {msg}")),
            ),
        };

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, that works for the most part! Just one test for a lint failing that I'll look into. That gets part of the way towards what I'd love to have, which is even leveraging the optimizations of inlining literals and similar.

@m-ou-se m-ou-se self-assigned this Nov 4, 2025
@m-ou-se
Copy link
Member

m-ou-se commented Nov 4, 2025

@m-ou-se as the formatting guru…Would it be feasible to leverage the internals of format_args! to inline literals and other format_args! calls, using fmt::Arguments::as_str to get the final string out? If it's feasible, it would provide more opportunity for optimization, though I'd need to re-benchmark to check as there would be more work being done.

My first impression of this PR is that it seems like a lot of complexity on the macro expansion side, and that it feels like this should be doable by using fmt::Arguments::as_str() (or as_statically_known_str()) instead. Have you tried anything like that? (E.g. an inline(always) function with an if let Some(m) = m.as_statically_known_str() dispatching to either panic() or panic_fmt()?)

@m-ou-se
Copy link
Member

m-ou-se commented Nov 4, 2025

A more extreme approach would be to optimise fmt::Arguments such that it's the same size as a &str and have a trivial conversion from &str to fmt::Arguments. Then panic() has no more advantage over panic_fmt(). I've been wanting to do that for years, but kept running into roadblocks. But I think many of them have been resolved; it is probably easier now. I'll investigate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-panic Area: Panicking machinery C-optimization Category: An issue highlighting optimization opportunities or PRs implementing such perf-regression Performance regression. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. 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.

8 participants