Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6d8226b

Browse files
committedAug 14, 2024
Auto merge of rust-lang#129102 - futile:experimental/proc-macro-caching, 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)
2 parents 355a307 + d47fa70 commit 6d8226b

File tree

19 files changed

+408
-51
lines changed

19 files changed

+408
-51
lines changed
 

‎Cargo.lock

+1
Original file line numberDiff line numberDiff line change
@@ -3688,6 +3688,7 @@ dependencies = [
36883688
"rustc_lexer",
36893689
"rustc_lint_defs",
36903690
"rustc_macros",
3691+
"rustc_middle",
36913692
"rustc_parse",
36923693
"rustc_serialize",
36933694
"rustc_session",

‎compiler/rustc_ast/src/tokenstream.rs

+98-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
//! ownership of the original.
1515
1616
use std::borrow::Cow;
17+
use std::hash::Hash;
1718
use std::{cmp, fmt, iter};
1819

1920
use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
2021
use rustc_data_structures::sync::{self, Lrc};
2122
use rustc_macros::{Decodable, Encodable, HashStable_Generic};
22-
use rustc_serialize::{Decodable, Encodable};
23+
use rustc_serialize::{Decodable, Encodable, Encoder};
24+
use rustc_span::def_id::{CrateNum, DefIndex};
2325
use rustc_span::{sym, Span, SpanDecoder, SpanEncoder, Symbol, DUMMY_SP};
2426

2527
use crate::ast::{AttrStyle, StmtKind};
@@ -140,6 +142,11 @@ impl fmt::Debug for LazyAttrTokenStream {
140142

141143
impl<S: SpanEncoder> Encodable<S> for LazyAttrTokenStream {
142144
fn encode(&self, _s: &mut S) {
145+
// FIXME(pr-time): Just a reminder that this exists/was tried out,
146+
// but probably not necessary anymore (see below).
147+
// self.to_attr_token_stream().encode(s)
148+
// We should not need to anymore, now that we `flatten`?
149+
// Yep, that seems to be true! :)
143150
panic!("Attempted to encode LazyAttrTokenStream");
144151
}
145152
}
@@ -296,6 +303,96 @@ pub struct AttrsTarget {
296303
#[derive(Clone, Debug, Default, Encodable, Decodable)]
297304
pub struct TokenStream(pub(crate) Lrc<Vec<TokenTree>>);
298305

306+
struct HashEncoder<H: std::hash::Hasher> {
307+
hasher: H,
308+
}
309+
310+
impl<H: std::hash::Hasher> Encoder for HashEncoder<H> {
311+
fn emit_usize(&mut self, v: usize) {
312+
self.hasher.write_usize(v)
313+
}
314+
315+
fn emit_u128(&mut self, v: u128) {
316+
self.hasher.write_u128(v)
317+
}
318+
319+
fn emit_u64(&mut self, v: u64) {
320+
self.hasher.write_u64(v)
321+
}
322+
323+
fn emit_u32(&mut self, v: u32) {
324+
self.hasher.write_u32(v)
325+
}
326+
327+
fn emit_u16(&mut self, v: u16) {
328+
self.hasher.write_u16(v)
329+
}
330+
331+
fn emit_u8(&mut self, v: u8) {
332+
self.hasher.write_u8(v)
333+
}
334+
335+
fn emit_isize(&mut self, v: isize) {
336+
self.hasher.write_isize(v)
337+
}
338+
339+
fn emit_i128(&mut self, v: i128) {
340+
self.hasher.write_i128(v)
341+
}
342+
343+
fn emit_i64(&mut self, v: i64) {
344+
self.hasher.write_i64(v)
345+
}
346+
347+
fn emit_i32(&mut self, v: i32) {
348+
self.hasher.write_i32(v)
349+
}
350+
351+
fn emit_i16(&mut self, v: i16) {
352+
self.hasher.write_i16(v)
353+
}
354+
355+
fn emit_raw_bytes(&mut self, s: &[u8]) {
356+
self.hasher.write(s)
357+
}
358+
}
359+
360+
impl<H: std::hash::Hasher> SpanEncoder for HashEncoder<H> {
361+
fn encode_span(&mut self, span: Span) {
362+
span.hash(&mut self.hasher)
363+
}
364+
365+
fn encode_symbol(&mut self, symbol: Symbol) {
366+
symbol.hash(&mut self.hasher)
367+
}
368+
369+
fn encode_expn_id(&mut self, expn_id: rustc_span::ExpnId) {
370+
expn_id.hash(&mut self.hasher)
371+
}
372+
373+
fn encode_syntax_context(&mut self, syntax_context: rustc_span::SyntaxContext) {
374+
syntax_context.hash(&mut self.hasher)
375+
}
376+
377+
fn encode_crate_num(&mut self, crate_num: CrateNum) {
378+
crate_num.hash(&mut self.hasher)
379+
}
380+
381+
fn encode_def_index(&mut self, def_index: DefIndex) {
382+
def_index.hash(&mut self.hasher)
383+
}
384+
385+
fn encode_def_id(&mut self, def_id: rustc_span::def_id::DefId) {
386+
def_id.hash(&mut self.hasher)
387+
}
388+
}
389+
390+
impl Hash for TokenStream {
391+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
392+
Encodable::encode(self, &mut HashEncoder { hasher: state });
393+
}
394+
}
395+
299396
/// Indicates whether a token can join with the following token to form a
300397
/// compound token. Used for conversions to `proc_macro::Spacing`. Also used to
301398
/// guide pretty-printing, which is where the `JointHidden` value (which isn't

‎compiler/rustc_expand/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ rustc_fluent_macro = { path = "../rustc_fluent_macro" }
2020
rustc_lexer = { path = "../rustc_lexer" }
2121
rustc_lint_defs = { path = "../rustc_lint_defs" }
2222
rustc_macros = { path = "../rustc_macros" }
23+
rustc_middle = { path = "../rustc_middle" }
2324
rustc_parse = { path = "../rustc_parse" }
2425
rustc_serialize = { path = "../rustc_serialize" }
2526
rustc_session = { path = "../rustc_session" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::cell::Cell;
2+
use std::ptr;
3+
4+
use rustc_ast::tokenstream::TokenStream;
5+
use rustc_data_structures::svh::Svh;
6+
use rustc_middle::ty::TyCtxt;
7+
use rustc_span::profiling::SpannedEventArgRecorder;
8+
use rustc_span::LocalExpnId;
9+
10+
use crate::base::ExtCtxt;
11+
use crate::errors;
12+
13+
pub(super) fn provide_derive_macro_expansion<'tcx>(
14+
tcx: TyCtxt<'tcx>,
15+
key: (LocalExpnId, Svh, &'tcx TokenStream),
16+
) -> Result<&'tcx TokenStream, ()> {
17+
let (invoc_id, _macro_crate_hash, input) = key;
18+
19+
let res = with_context(|(ecx, client)| {
20+
let span = invoc_id.expn_data().call_site;
21+
let _timer = ecx.sess.prof.generic_activity_with_arg_recorder(
22+
"expand_derive_proc_macro_inner",
23+
|recorder| {
24+
recorder.record_arg_with_span(ecx.sess.source_map(), ecx.expansion_descr(), span);
25+
},
26+
);
27+
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace;
28+
let strategy = crate::proc_macro::exec_strategy(ecx);
29+
let server = crate::proc_macro_server::Rustc::new(ecx);
30+
let res = match client.run(&strategy, server, input.clone(), proc_macro_backtrace) {
31+
// FIXME(pr-time): without flattened some (weird) tests fail, but no idea if it's correct/enough
32+
Ok(stream) => Ok(tcx.arena.alloc(stream.flattened()) as &TokenStream),
33+
Err(e) => {
34+
ecx.dcx().emit_err({
35+
errors::ProcMacroDerivePanicked {
36+
span,
37+
message: e.as_str().map(|message| errors::ProcMacroDerivePanickedHelp {
38+
message: message.into(),
39+
}),
40+
}
41+
});
42+
Err(())
43+
}
44+
};
45+
res
46+
});
47+
48+
res
49+
}
50+
51+
type CLIENT = pm::bridge::client::Client<pm::TokenStream, pm::TokenStream>;
52+
53+
// based on rust/compiler/rustc_middle/src/ty/context/tls.rs
54+
thread_local! {
55+
/// A thread local variable that stores a pointer to the current `CONTEXT`.
56+
static TLV: Cell<(*mut (), Option<CLIENT>)> = const { Cell::new((ptr::null_mut(), None)) };
57+
}
58+
59+
#[inline]
60+
fn erase(context: &mut ExtCtxt<'_>) -> *mut () {
61+
context as *mut _ as *mut ()
62+
}
63+
64+
#[inline]
65+
unsafe fn downcast<'a>(context: *mut ()) -> &'a mut ExtCtxt<'a> {
66+
unsafe { &mut *(context as *mut ExtCtxt<'a>) }
67+
}
68+
69+
#[inline]
70+
fn enter_context_erased<F, R>(erased: (*mut (), Option<CLIENT>), f: F) -> R
71+
where
72+
F: FnOnce() -> R,
73+
{
74+
TLV.with(|tlv| {
75+
let old = tlv.replace(erased);
76+
let _reset = rustc_data_structures::defer(move || tlv.set(old));
77+
f()
78+
})
79+
}
80+
81+
/// Sets `context` as the new current `CONTEXT` for the duration of the function `f`.
82+
#[inline]
83+
pub fn enter_context<'a, F, R>(context: (&mut ExtCtxt<'a>, CLIENT), f: F) -> R
84+
where
85+
F: FnOnce() -> R,
86+
{
87+
let (ectx, client) = context;
88+
let erased = (erase(ectx), Some(client));
89+
enter_context_erased(erased, f)
90+
}
91+
92+
/// Allows access to the current `CONTEXT` in a closure if one is available.
93+
#[inline]
94+
#[track_caller]
95+
pub fn with_context_opt<F, R>(f: F) -> R
96+
where
97+
F: for<'a, 'b> FnOnce(Option<&'b mut (&mut ExtCtxt<'a>, CLIENT)>) -> R,
98+
{
99+
let (ectx, client_opt) = TLV.get();
100+
if ectx.is_null() {
101+
f(None)
102+
} else {
103+
// We could get an `CONTEXT` pointer from another thread.
104+
// Ensure that `CONTEXT` is `DynSync`.
105+
// FIXME(pr-time): we should not be able to?
106+
// sync::assert_dyn_sync::<CONTEXT<'_>>();
107+
108+
// prevent double entering, as that would allow creating two `&mut ExtCtxt`s
109+
// FIXME(pr-time): probably use a RefCell instead (which checks this properly)?
110+
enter_context_erased((ptr::null_mut(), None), || unsafe {
111+
let ectx = downcast(ectx);
112+
f(Some(&mut (ectx, client_opt.unwrap())))
113+
})
114+
}
115+
}
116+
117+
/// Allows access to the current `CONTEXT`.
118+
/// Panics if there is no `CONTEXT` available.
119+
#[inline]
120+
pub fn with_context<F, R>(f: F) -> R
121+
where
122+
F: for<'a, 'b> FnOnce(&'b mut (&mut ExtCtxt<'a>, CLIENT)) -> R,
123+
{
124+
with_context_opt(|opt_context| f(opt_context.expect("no CONTEXT stored in tls")))
125+
}

‎compiler/rustc_expand/src/expand.rs

+19-17
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
488488
self.cx.force_mode = force;
489489

490490
let fragment_kind = invoc.fragment_kind;
491-
match self.expand_invoc(invoc, &ext.kind) {
491+
match self.expand_invoc(invoc, &ext) {
492492
ExpandResult::Ready(fragment) => {
493493
let mut derive_invocations = Vec::new();
494494
let derive_placeholders = self
@@ -650,7 +650,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
650650
fn expand_invoc(
651651
&mut self,
652652
invoc: Invocation,
653-
ext: &SyntaxExtensionKind,
653+
ext: &Lrc<SyntaxExtension>,
654654
) -> ExpandResult<AstFragment, Invocation> {
655655
let recursion_limit = match self.cx.reduced_recursion_limit {
656656
Some((limit, _)) => limit,
@@ -671,7 +671,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
671671

672672
let (fragment_kind, span) = (invoc.fragment_kind, invoc.span());
673673
ExpandResult::Ready(match invoc.kind {
674-
InvocationKind::Bang { mac, span } => match ext {
674+
InvocationKind::Bang { mac, span } => match &ext.kind {
675675
SyntaxExtensionKind::Bang(expander) => {
676676
match expander.expand(self.cx, span, mac.args.tokens.clone()) {
677677
Ok(tok_result) => {
@@ -701,7 +701,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
701701
}
702702
_ => unreachable!(),
703703
},
704-
InvocationKind::Attr { attr, pos, mut item, derives } => match ext {
704+
InvocationKind::Attr { attr, pos, mut item, derives } => match &ext.kind {
705705
SyntaxExtensionKind::Attr(expander) => {
706706
self.gate_proc_macro_input(&item);
707707
self.gate_proc_macro_attr_item(span, &item);
@@ -780,10 +780,10 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
780780
}
781781
_ => unreachable!(),
782782
},
783-
InvocationKind::Derive { path, item, is_const } => match ext {
783+
InvocationKind::Derive { path, item, is_const } => match &ext.kind {
784784
SyntaxExtensionKind::Derive(expander)
785785
| SyntaxExtensionKind::LegacyDerive(expander) => {
786-
if let SyntaxExtensionKind::Derive(..) = ext {
786+
if let SyntaxExtensionKind::Derive(..) = ext.kind {
787787
self.gate_proc_macro_input(&item);
788788
}
789789
// The `MetaItem` representing the trait to derive can't
@@ -794,6 +794,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
794794
span,
795795
path,
796796
};
797+
invoc.expansion_data.id.expn_data();
797798
let items = match expander.expand(self.cx, span, &meta, item, is_const) {
798799
ExpandResult::Ready(items) => items,
799800
ExpandResult::Retry(item) => {
@@ -810,18 +811,19 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
810811
},
811812
InvocationKind::GlobDelegation { item } => {
812813
let AssocItemKind::DelegationMac(deleg) = &item.kind else { unreachable!() };
813-
let suffixes = match ext {
814-
SyntaxExtensionKind::GlobDelegation(expander) => match expander.expand(self.cx)
815-
{
816-
ExpandResult::Ready(suffixes) => suffixes,
817-
ExpandResult::Retry(()) => {
818-
// Reassemble the original invocation for retrying.
819-
return ExpandResult::Retry(Invocation {
820-
kind: InvocationKind::GlobDelegation { item },
821-
..invoc
822-
});
814+
let suffixes = match &ext.kind {
815+
SyntaxExtensionKind::GlobDelegation(expander) => {
816+
match expander.expand(self.cx) {
817+
ExpandResult::Ready(suffixes) => suffixes,
818+
ExpandResult::Retry(()) => {
819+
// Reassemble the original invocation for retrying.
820+
return ExpandResult::Retry(Invocation {
821+
kind: InvocationKind::GlobDelegation { item },
822+
..invoc
823+
});
824+
}
823825
}
824-
},
826+
}
825827
SyntaxExtensionKind::LegacyBang(..) => {
826828
let msg = "expanded a dummy glob delegation";
827829
let guar = self.cx.dcx().span_delayed_bug(span, msg);

‎compiler/rustc_expand/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ mod proc_macro_server;
2828
pub use mbe::macro_rules::compile_declarative_macro;
2929
pub mod base;
3030
pub mod config;
31+
pub(crate) mod derive_macro_expansion;
3132
pub mod expand;
3233
pub mod module;
3334
// FIXME(Nilstrieb) Translate proc_macro diagnostics
3435
#[allow(rustc::untranslatable_diagnostic)]
3536
pub mod proc_macro;
3637

38+
pub fn provide(providers: &mut rustc_middle::util::Providers) {
39+
providers.derive_macro_expansion = derive_macro_expansion::provide_derive_macro_expansion;
40+
}
41+
3742
rustc_fluent_macro::fluent_messages! { "../messages.ftl" }
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.