-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Compute query::Providers
almost entirely at compile-time.
#74283
Conversation
@bors rollup=never (in case this breaks some tool or fulldeps test, and for perf) |
@bors try @rust-timer queue |
Awaiting bors try build completion |
⌛ Trying commit 77e4bef6bc791bc166021050f8e5d5b618645ea1 with merge 46100ef145a1aca4b68beed35e7314f9a5507928... |
💔 Test failed - checks-actions |
Oh I was worried about this, but didn't realize it changed in this release cycle: error[E0764]: mutable references are not allowed in constants
--> src/librustc_interface/passes.rs:724:25
|
724 | let mut providers = &mut Providers::EMPTY;
| ^^^^^^^^^^^^^^^^^^^^^ `&mut` is only allowed in `const fn`
error[E0764]: mutable references are not allowed in constants
--> src/librustc_interface/passes.rs:749:21
|
749 | let providers = &mut DEFAULT_QUERY_PROVIDERS;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&mut` is only allowed in `const fn` |
@bors try @rust-timer queue |
Awaiting bors try build completion |
⌛ Trying commit f0141eb with merge 8e025f12c56194c5c38b7e718c3f1498028d7d94... |
☀️ Try build successful - checks-actions, checks-azure |
Queued 8e025f12c56194c5c38b7e718c3f1498028d7d94 with parent 9d09331, future comparison URL. |
rustc_codegen_ssa::provide(providers); | ||
} | ||
pub const DEFAULT_QUERY_PROVIDERS: Providers = { | ||
// FIXME(eddyb) the `const fn` shouldn't be needed, but mutable references |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads as if it should be a fn
instead, but I think what you actually mean is that it should be directly inlined in DEFAULT_QUERY_PROVIDERS
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, maybe I should say "separate function" or something like that.
Finished benchmarking try commit (8e025f12c56194c5c38b7e718c3f1498028d7d94): comparison url. |
This is a slight (up to 0.9%) slowdown especially for check benchmarks. |
Okay, that's weird, I was expecting a speed-up. Since all it needs to do now is |
This comment has been minimized.
This comment has been minimized.
@lcnr compare with the old code, which also kept One thing I wasn't aware of was that LLVM can actually optimize the So the difference is that before we were writing every field twice, once by Nevermind, I think I found the problem, it's something I was worried might happen but didn't think it could. So a EDIT: most of the providers appear to be EDIT2: confirmed, it's closures that are causing the excessive cross-crate codegen. I'll look for a potential way to solve this. |
It looks like closures are considered generic functions, due to ( I can try and fix the closure monomorphization behavior, although we might want to land it separately, there's clearly some risk of it limiting some optimizations, although not for all the cases in which a closure is found within a generic function, and the only other case would have to be |
I've played around with changing the closure codegen behavior (to fit this situation better), and I don't think it's worth it. What's happening is arguably not a bug, but a feature, we just lose a bit of performance due to the closure-provided queries having duplicate codegen in If we want to rely on CTFE I believe there's only one way: introduce a We could even use the same I'd appreciate feedback on that, but otherwise it'd be probably fine to just have a While messing with closure codegen, I've come across a few things that I'll try to split into several PRs (aren't rabbit holes fun?). |
I think I'd prefer that. All the other approaches don't seem strictly better than the status quo (modulo getting the current PR to work fast, but that rabbit hole has been explored). Without a clear improvement over the status quo, I personally prefer a simpler solution over a complex one, even if the complex one doesn't require lazy statics. |
@oli-obk In case it wasn't clear: the macro idea should make the current PR to work, by removing all closures (i.e. the macro can turn closure syntax into regular functions under the hook, which would not trigger the problematic behavior this PR stumbled onto). |
oh... hmm... ok that works, just leave a FIXME for removing the macro once the closure problem is resolved (do we have an issue for that?) |
The macro would be slightly nicer than today (and more declarative, so it would work with approaches other than " And the closure thing we'll likely not fix, as it's arguably largely not a bug: it's free devirtualization, it just happens to slightly regress performance in this very specific case. |
I was under the impression that this may be duplicating closure bodies all over the place, causing them to get compiled twice when we should just be referring to the already compiled function from the other crate. |
@oli-obk Yes, that's typically a good idea because we want to inline closures. And it barely hurts rustc, presumably they just get less optimized in |
I implemented this in #74347. |
…u, r=eddyb Initialize default providers only once This avoids copying a new `Providers` struct for each downstream crate that wants to use it. Follow-up to rust-lang#74283 without the perf hit. r? @eddyb
Question: if instead of Anyway, I don't know that I prefer this approach to what is being proposed here, I was just curious though if it would have obvious performance implications in comparison to |
(It would be ergonomically annoying to write |
Also, fwiw, I am not a fan of having providers provided via closures. I always find it hard to find their definitions that way, for one thing, and it feels like it clumps many providers into one function. I prefer if people use a free function with the same name as the query, so that it is easy to find. |
That would double the size of |
@nikomatsakis Would be interesting if we had a quick way of measuring the performance impact of that (I guess we can do the grunt work of writing I'm not as worried about anything happening before Also, we kind of want to keep providers as pure as possible, but using |
i.e., you expect the extra load involved in dispatch through a vtable to be very expensive? |
Maybe, but maybe I'm just assuming. It's definitely at least as expensive, but it could be marginal, depending on workload. I remember writing a longer reply but I think I forgot to send it and it got lost ages ago. I forget what I wanted to do with this PR. I think I'll just close it but not delete the branch, so anyone can refer to it later. |
Background
Query "providers" are the functions which actually implement the computation of a query, when the result wasn't already cached.
To allow queries to be implemented "downstream" from the definition of the query system, and even crates using the query, their providers are kept as
fn
pointers in astruct Providers
, which is filled at runtime:To further make filling the
Providers
struct ergonomic, and hide implementation details,fn provide
functions are used, which mutate one or morefn
pointer fields, in order to "provide" query implementations:rustc_interface
ties all of this together, by calling each crate's top-levelfn provide
functions, to create a completeProviders
, which is then immutably stored within the query system (which ends up inside/referenced byTyCtxt
).Motivation
rustc_interface
's API allows custom drivers to override query providers, to customize the behavior of certain queries.E.g.: @jyn514 used this in #73566 to prevent
rustdoc
from doing too much work (and producing unwanted errors).However, if executing the original provider (also called "default", although not in the sense of
Providers
implementingDefault
, but rather the result ofrustc_interface
combining all thefn provide
together) is desired, there is no nice way to do so.The main approach today is to use a
lazy_static!
/thread_local!
to cache one or more providers, by running one or morefn provide
, and then extracting some of thefn
pointer fields, or even preserving the entire resultingProviders
.Sometimes the actual provider function is publicly exposed, but ideally it should remain hidden, to avoid accidental calls (which would bypass the query cache), or dead code remaining around even when it's no longer used through the query system.
It's also possible that there's some startup cost to constructing the
Providers
at runtime, but likely not enough to matter.This PR
All
fn provide
are nowconst fn
, and they're ran at compile-time to produceconst DEFAULT_QUERY_PROVIDERS
.(The one exception is the codegen backend, due to it potentially varying at runtime, but e.g.
rustc_codegen_llvm
only installs a grand total of 2 providers, so it likely does not matter much)The effect of this is that the following snippet optimizes to returning a constant
fn
pointer:Probably the best way to grab one of the default providers, however, is this:
As
DEFAULT_TYPE_OF
is computed entirely at compile-time, any use of it will directly refer to the (private) function which implements thetype_of
query, even with no LLVM optimizations involved.(And you can directly use it to call that function, e.g.
DEFAULT_TYPE_OF(tcx, def_id)
)r? @nikomatsakis cc @nnethercote
(also cc @rust-lang/wg-const-eval because of the
feature(const_mut_refs)
dogfooding involved)