Skip to content

perf: fold all late lint passes into one statically-combined pass - #17124

Merged
blyxyas merged 1 commit into
rust-lang:masterfrom
xmakro:perf/static-combine
Jun 1, 2026
Merged

perf: fold all late lint passes into one statically-combined pass#17124
blyxyas merged 1 commit into
rust-lang:masterfrom
xmakro:perf/static-combine

Conversation

@xmakro

@xmakro xmakro commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Replaces clippy's per-pass late-lint dispatch (one indirect dyn vtable call per registered pass, per HIR node) with a single statically-combined late pass. This means empty default method bodies DCE away, so the surviving overrides become inlined calls behind predicted branches instead of indirect dispatch.

Benchmarked with N=10, medians:

crate instructions branches branch-misses cycles wall
tokio −9.1% −13.2% −48.2% −21.5% −20.9%
ripgrep −10.3% −14.7% −57.4% −27.6% −25.9%
regex −5.9% −8.5% −33.1% −17.4% −15.7%
syn −9.6% −13.8% −56.6% −24.2% −22.9%
serde −10.7% −15.2% −65.1% −23.8% −22.8%
wasmi −6.3% −9.2% −60.8% −19.6% −18.9%

I verified diagnostics are identical for all benchmarked crates.

I also adapted cargo dev new_lint to insert new late passes correctly with the updated syntax.

changelog: none

@xmakro
xmakro force-pushed the perf/static-combine branch 4 times, most recently from 234594b to 0e3e477 Compare June 1, 2026 00:48
@xmakro
xmakro marked this pull request as ready for review June 1, 2026 02:20
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jun 1, 2026
@rustbot

rustbot commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @samueltardieu (or someone else) some time within the next two weeks.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue
Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: 8 candidates
  • 8 candidates expanded to 8 candidates
  • Random selection from Jarcho, ada4a, dswij, llogiq, samueltardieu

@xmakro

xmakro commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

cc @blyxyas, @y21 and @flip1995 who might have comments on this approach

@xmakro
xmakro force-pushed the perf/static-combine branch 3 times, most recently from b87cc14 to 08fe0b6 Compare June 1, 2026 06:01
@samueltardieu

Copy link
Copy Markdown
Member

r? @blyxyas
@rustbot label G-performance-project

@rustbot rustbot assigned blyxyas and unassigned samueltardieu Jun 1, 2026
@rustbot

rustbot commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

blyxyas is not on the review rotation at the moment.
They may take a while to respond.

@rustbot rustbot added the G-performance-project Goal: For issues and PRs related to the Clippy Performance Project label Jun 1, 2026
@blyxyas

blyxyas commented Jun 1, 2026

Copy link
Copy Markdown
Member

I've verified the performance improvements, although not as pronounced as in the pull request's description, they're very much appreciable!

Thank you for this contribution!, Having so many empty bodies were a pain point and something that I wanted to optimize, but didn't know exactly how.

I'll review the PR itself now, and if everything's correct, it'll be merged.

@blyxyas blyxyas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Everything seems good, just a nit!

View changes since this review

Comment thread clippy_lints/src/combined_late_pass.rs Outdated
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Jun 1, 2026
@rustbot

rustbot commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

Clippy registers ~300 late passes, each as its own boxed `dyn LateLintPass`.
Every `check_*` call is therefore an indirect (vtable) dispatch, even for
the many passes that don't override that method and just run the empty
default. Walking the HIR multiplies this across every node.

Fold all late passes into a single statically-combined struct: one concrete
field per pass, with a single `LateLintPass` impl that forwards each
`check_*` to every field in turn. Because the field types are concrete and
the calls are `#[inline(always)]`, passes that don't override a method
contribute a DCE'd empty body and the ones that do become direct, inlined
calls. No vtable, no per-node dynamic dispatch, and no monomorphized cons-list
to bloat the binary.

Each field is wrapped in a `Gated` carrying a precomputed `active` flag
derived from the same `lints_that_dont_need_to_run` predicate rustc uses to
drop fully-disabled passes, so allow-by-default lints are skipped by a
well-predicted branch rather than executed. The `LateLintPass` impl is
generated from rustc's `late_lint_methods!` list so the two can never drift.

`cargo dev new_lint` now emits a combined-struct field for new late passes
at the in-list marker.

Measured on an idle machine, same binary, byte-identical diagnostics over a
6-crate corpus (tokio, ripgrep, regex, syn, serde, wasmi): cycles -17% to
-28%, instructions -6% to -11%, branch-misses -33% to -65%. lintcheck over
the 25-crate default corpus is byte-identical to stock.

changelog: none
@xmakro
xmakro force-pushed the perf/static-combine branch from 08fe0b6 to 294e68c Compare June 1, 2026 14:21
@xmakro

xmakro commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the prompt review @blyxyas! I addressed your comment.

For the performance improvement, how are you benchmarking? What CPU are you running on? Did the instruction count reduction match? And what was the wall clock time reduction on your machine? I measured on AMD Ryzen 9 9950X3D (Zen 5).

Once this is merged, I can send a follow up PR to apply the same approach for the early passes.

@blyxyas

blyxyas commented Jun 1, 2026

Copy link
Copy Markdown
Member

how are you benchmarking? What CPU are you running on? Did the instruction count reduction match? And what was the wall clock time reduction on your machine?

I'm benchmarking via Valgrind, specifically Callgrind testing your commit and the commit prior to your commit. Both built in release. This is the command I'm using:

CARGO_TARGET_DIR="/tmp/m2" LD_LIBRARY_PATH="$(rustc --print=sysroot)/lib" valgrind --tool=callgrind --trace-children=yes ../../../release/cargo-clippy

In ripgrep for example I reported a 8% performance improvement both in icount and in wall time. Which is great!
My CPU is AMD Ryzen 7 5800X (16 threads) @ 4.85 GHz. Still, be it 10% or 20% performance improvement, it's great.

I'll give it a second review

@blyxyas blyxyas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks! ❤️

View changes since this review

@blyxyas
blyxyas added this pull request to the merge queue Jun 1, 2026
Merged via the queue into rust-lang:master with commit 68d2b73 Jun 1, 2026
11 checks passed
xmakro added a commit to xmakro/rust-clippy that referenced this pull request Jun 1, 2026
Clippy registers about 50 early passes, each as its own boxed
`dyn EarlyLintPass`. Every `check_*` call is an indirect (vtable) dispatch,
even for the many passes that don't override that method and just run the
empty default. Walking the AST multiplies this across every node.

Fold all early passes into a single statically-combined struct: one concrete
field per pass, with a single `EarlyLintPass` impl that forwards each
`check_*` to every field in turn. Because the field types are concrete and the
calls are `#[inline(always)]`, passes that don't override a method contribute a
DCE'd empty body and the ones that do become direct, inlined calls. No vtable
and no per-node dynamic dispatch.

This mirrors the late-pass combine from rust-lang#17124, but with no `Gated` active
flag. rustc drops fully-disabled late passes via `lints_that_dont_need_to_run`,
but the early pass runner has no such filtering, so a plain forward is
equivalent and a gate would buy nothing. The `EarlyLintPass` impl is generated
from rustc's `early_lint_methods!` list so the two cannot drift.

`cargo dev new_lint` now emits a combined-struct field for new early passes at
the in-list marker.

Measured the same way as rust-lang#17124 (valgrind callgrind, release build, this commit
against its parent, re-linting the ripgrep 14.1.0 workspace): clippy-driver
instructions drop about 3.9%, from 1317445596 to 1266296487. The full UI test
suite and dogfood pass unchanged.

changelog: none
@flip1995

flip1995 commented Jun 1, 2026

Copy link
Copy Markdown
Member

changelog: none

Awesome work! Only comment I have is, that you should've added a changelog entry. This kind of perf improvement is definitely worth mentioning in our changelog. Maybe do that in the PR for early lint passes.

tupe12334 pushed a commit to tupe12334/rust-clippy that referenced this pull request Jun 2, 2026
…ust-lang#17132)

Clippy registers about 50 early passes, each as its own boxed `dyn
EarlyLintPass`. Every `check_*` call is an indirect (vtable) dispatch,
even for the many passes that don't override that method and just run
the empty default. Walking the AST multiplies this across every node.

This folds all early passes into a single statically-combined struct:
one concrete field per pass, with a single `EarlyLintPass` impl that
forwards each `check_*` to every field in turn. Because the field types
are concrete and the calls are `#[inline(always)]`, passes that don't
override a method contribute a DCE'd empty body and the ones that do
become direct, inlined calls. No vtable and no per-node dynamic
dispatch.

This mirrors the late-pass combine from rust-lang#17124, but
with no `Gated` active flag. rustc drops fully-disabled late passes via
`lints_that_dont_need_to_run`, but the early pass runner has no such
filtering. The `EarlyLintPass` impl is generated from rustc's
`early_lint_methods!` list so the two cannot drift.

`cargo dev new_lint` now emits a combined-struct field for new early
passes at the in-list marker.

Measured the same way as rust-lang#17124 (valgrind
callgrind, release build, this branch against its parent, re-linting the
ripgrep 14.1.0 workspace): clippy-driver instructions drop about 3.9%,
from 1317445596 to 1266296487. The improvement is smaller than the
late-pass combine because there are far fewer early passes and the AST
walk is lighter than the HIR walk. The UI test suite and dogfood pass
unchanged.

changelog: none
@xmakro

xmakro commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

changelog: none

Awesome work! Only comment I have is, that you should've added a changelog entry. This kind of perf improvement is definitely worth mentioning in our changelog. Maybe do that in the PR for early lint passes.

Thanks! The early lint pass was already merged. Is it possible for you to add an entry to the changelog manually? There are a few more PRs with smaller savings that could all be summarized under one changelog item.

@flip1995 flip1995 added beta-accepted Accepted for backporting to the compiler in the beta channel. and removed beta-accepted Accepted for backporting to the compiler in the beta channel. labels Jun 3, 2026
@flip1995

flip1995 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Thanks! The early lint pass was already merged. Is it possible for you to add an entry to the changelog manually? There are a few more PRs with smaller savings that could all be summarized under one changelog item.

Only if we remember ~10 weeks from now when the changelog is written. Pinging @alex-semenyuk who usually writes the changelog. This will need to be included in the 1.98.0 changelog.

I added the beta-accepted label, since those are checked during changelog writing anyway. But this is for 1.98, not 1.97.

@ada4a

ada4a commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Could you please also update the docs at

## Lint registration
If we run the `cargo dev new_lint` command for a new lint, the lint will be
automatically registered and there is nothing more to do.
However, sometimes we might want to declare a new lint by hand. In this case,
we'd use `cargo dev update_lints` command afterwards.
When a lint is manually declared, we might need to register the lint pass
manually in the `register_lints` function in `clippy_lints/src/lib.rs`:
```rust
store.register_late_pass(|_| Box::new(foo_functions::FooFunctions));
```
As you might have guessed, where there's something late, there is something
early: in Clippy there is a `register_early_pass` method as well. More on early
vs. late passes in the [Lint Passes] chapter.
Without a call to one of `register_early_pass` or `register_late_pass`, the lint
pass in question will not be run.

to reflect this change? Thanks in advance:)

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

Labels

beta-accepted Accepted for backporting to the compiler in the beta channel. G-performance-project Goal: For issues and PRs related to the Clippy Performance Project S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants