forked from rust-lang/rust
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto merge of rust-lang#129102 - futile:experimental/proc-macro-cachi…
…ng, r=<try> Experimental: Add Derive Proc-Macro Caching # On-Disk Caching For Derive Proc-Macro Invocations This PR adds on-disk caching for derive proc-macro invocations using rustc's query system to speed up incremental compilation. The implementation is (intentionally) a bit rough/incomplete, as I wanted to see whether this helps with performance before fully implementing it/RFCing etc. I did some ad-hoc performance testing. ## Rough, Preliminary Eval Results: Using a version built through `DEPLOY=1 src/ci/docker/run.sh dist-x86_64-linux` (which I got from [here](https://rustc-dev-guide.rust-lang.org/building/optimized-build.html#profile-guided-optimization)). ### [Some Small Personal Project](https://github.com/futile/ultra-game): ```console # with -Zthreads=0 as well $ touch src/main.rs && cargo +dist check ``` Caused a re-check of 1 crate (the only one). Result: | Configuration | Time (avg. ~5 runs) | |--------|--------| | Uncached | ~0.54s | | Cached | ~0.54s | No visible difference. ### [Bevy](https://github.com/bevyengine/bevy): ```console $ touch crates/bevy_ecs/src/lib.rs && cargo +dist check ``` Caused a re-check of 29 crates. Result: | Configuration | Time (avg. ~5 runs) | |--------|--------| | Uncached | ~6.4s | | Cached | ~5.3s | Roughly 1s, or ~17% speedup. ### [Polkadot-Sdk](https://github.com/paritytech/polkadot-sdk): Basically this script (not mine): https://github.com/coderemotedotdev/rustc-profiles/blob/d61ad38c496459d82e35d8bdb0a154fbb83de903/scripts/benchmark_incremental_builds_polkadot_sdk.sh TL;DR: Two full `cargo check` runs to fill the incremental caches (for cached & uncached). Then 10 repetitions of `touch $some_file && cargo +uncached check && cargo +cached check`. ```console $ cargo update # `time` didn't build because compiler too new/dep too old $ ./benchmark_incremental_builds_polkadot_sdk.sh # see above ``` _Huge_ workspace with ~190 crates. Not sure how many were re-built/re-checkd on each invocation. Result: | Configuration | Time (avg. 10 runs) | |--------|--------| | Uncached | 99.4s | | Cached | 67.5s | Very visible speedup of 31.9s or ~32%. --- **-> Based on these results I think it makes sense to do a rustc-perf run and see what that reports.** --- ## Current Limitations/TODOs I left some `FIXME(pr-time)`s in the code for things I wanted to bring up/draw attention to in this PR. Usually when I wasn't sure if I found a (good) solution or when I knew that there might be a better way to do something; See the diff for these. ### High-Level Overview of What's Missing For "Real" Usage: * [ ] Add caching for `Bang`- and `Attr`-proc macros (currently only `Derive`). * Not a big change, I just focused on `derive`-proc macros for now, since I felt like these should be most cacheable and are used very often in practice. * [ ] Allow marking specific macros as "do not cache" (currently only all-or-nothing). * Extend the unstable option to support, e.g., `-Z cache-derive-macros=some_pm_crate::some_derive_macro_fn` for easy testing using the nightly compiler. * After Testing: Add a `#[proc_macro_cacheable]` annotation to allow proc-macro authors to "opt-in" to caching (or sth. similar). Would probably need an RFC? * Might make sense to try to combine this with rust-lang#99515, so that external dependencies can be picked up and be taken into account as well. --- So, just since you were in the loop on the attempt to cache declarative macro expansions: r? `@petrochenkov` Please feel free to re-/unassign! Finally: I hope this isn't too big a PR, I'll also show up in Zulip since I read that that is usually appreciated. Thanks a lot for taking a look! :) (Kind of related/very similar approach, old declarative macro caching PR: rust-lang#128747)
- Loading branch information
Showing
19 changed files
with
408 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
use std::cell::Cell; | ||
use std::ptr; | ||
|
||
use rustc_ast::tokenstream::TokenStream; | ||
use rustc_data_structures::svh::Svh; | ||
use rustc_middle::ty::TyCtxt; | ||
use rustc_span::profiling::SpannedEventArgRecorder; | ||
use rustc_span::LocalExpnId; | ||
|
||
use crate::base::ExtCtxt; | ||
use crate::errors; | ||
|
||
pub(super) fn provide_derive_macro_expansion<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
key: (LocalExpnId, Svh, &'tcx TokenStream), | ||
) -> Result<&'tcx TokenStream, ()> { | ||
let (invoc_id, _macro_crate_hash, input) = key; | ||
|
||
let res = with_context(|(ecx, client)| { | ||
let span = invoc_id.expn_data().call_site; | ||
let _timer = ecx.sess.prof.generic_activity_with_arg_recorder( | ||
"expand_derive_proc_macro_inner", | ||
|recorder| { | ||
recorder.record_arg_with_span(ecx.sess.source_map(), ecx.expansion_descr(), span); | ||
}, | ||
); | ||
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace; | ||
let strategy = crate::proc_macro::exec_strategy(ecx); | ||
let server = crate::proc_macro_server::Rustc::new(ecx); | ||
let res = match client.run(&strategy, server, input.clone(), proc_macro_backtrace) { | ||
// FIXME(pr-time): without flattened some (weird) tests fail, but no idea if it's correct/enough | ||
Ok(stream) => Ok(tcx.arena.alloc(stream.flattened()) as &TokenStream), | ||
Err(e) => { | ||
ecx.dcx().emit_err({ | ||
errors::ProcMacroDerivePanicked { | ||
span, | ||
message: e.as_str().map(|message| errors::ProcMacroDerivePanickedHelp { | ||
message: message.into(), | ||
}), | ||
} | ||
}); | ||
Err(()) | ||
} | ||
}; | ||
res | ||
}); | ||
|
||
res | ||
} | ||
|
||
type CLIENT = pm::bridge::client::Client<pm::TokenStream, pm::TokenStream>; | ||
|
||
// based on rust/compiler/rustc_middle/src/ty/context/tls.rs | ||
thread_local! { | ||
/// A thread local variable that stores a pointer to the current `CONTEXT`. | ||
static TLV: Cell<(*mut (), Option<CLIENT>)> = const { Cell::new((ptr::null_mut(), None)) }; | ||
} | ||
|
||
#[inline] | ||
fn erase(context: &mut ExtCtxt<'_>) -> *mut () { | ||
context as *mut _ as *mut () | ||
} | ||
|
||
#[inline] | ||
unsafe fn downcast<'a>(context: *mut ()) -> &'a mut ExtCtxt<'a> { | ||
unsafe { &mut *(context as *mut ExtCtxt<'a>) } | ||
} | ||
|
||
#[inline] | ||
fn enter_context_erased<F, R>(erased: (*mut (), Option<CLIENT>), f: F) -> R | ||
where | ||
F: FnOnce() -> R, | ||
{ | ||
TLV.with(|tlv| { | ||
let old = tlv.replace(erased); | ||
let _reset = rustc_data_structures::defer(move || tlv.set(old)); | ||
f() | ||
}) | ||
} | ||
|
||
/// Sets `context` as the new current `CONTEXT` for the duration of the function `f`. | ||
#[inline] | ||
pub fn enter_context<'a, F, R>(context: (&mut ExtCtxt<'a>, CLIENT), f: F) -> R | ||
where | ||
F: FnOnce() -> R, | ||
{ | ||
let (ectx, client) = context; | ||
let erased = (erase(ectx), Some(client)); | ||
enter_context_erased(erased, f) | ||
} | ||
|
||
/// Allows access to the current `CONTEXT` in a closure if one is available. | ||
#[inline] | ||
#[track_caller] | ||
pub fn with_context_opt<F, R>(f: F) -> R | ||
where | ||
F: for<'a, 'b> FnOnce(Option<&'b mut (&mut ExtCtxt<'a>, CLIENT)>) -> R, | ||
{ | ||
let (ectx, client_opt) = TLV.get(); | ||
if ectx.is_null() { | ||
f(None) | ||
} else { | ||
// We could get an `CONTEXT` pointer from another thread. | ||
// Ensure that `CONTEXT` is `DynSync`. | ||
// FIXME(pr-time): we should not be able to? | ||
// sync::assert_dyn_sync::<CONTEXT<'_>>(); | ||
|
||
// prevent double entering, as that would allow creating two `&mut ExtCtxt`s | ||
// FIXME(pr-time): probably use a RefCell instead (which checks this properly)? | ||
enter_context_erased((ptr::null_mut(), None), || unsafe { | ||
let ectx = downcast(ectx); | ||
f(Some(&mut (ectx, client_opt.unwrap()))) | ||
}) | ||
} | ||
} | ||
|
||
/// Allows access to the current `CONTEXT`. | ||
/// Panics if there is no `CONTEXT` available. | ||
#[inline] | ||
pub fn with_context<F, R>(f: F) -> R | ||
where | ||
F: for<'a, 'b> FnOnce(&'b mut (&mut ExtCtxt<'a>, CLIENT)) -> R, | ||
{ | ||
with_context_opt(|opt_context| f(opt_context.expect("no CONTEXT stored in tls"))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.