Skip to content

Commit c3f3550

Browse files
committedJul 19, 2022
Auto merge of #98189 - mystor:fast_ident_literal, r=eddyb
proc_macro/bridge: stop using a remote object handle for proc_macro Ident and Literal This is the fourth part of #86822, split off as requested in #86822 (review). This patch transforms the `Ident` and `Group` types into structs serialized over IPC rather than handles. Symbol values are interned on both the client and server when deserializing, to avoid unnecessary string copies and keep the size of `TokenTree` down. To do the interning efficiently on the client, the proc-macro crate is given a vendored version of the fxhash hasher, as `SipHash` appeared to cause performance issues. This was done rather than depending on `rustc_hash` as it is unfortunately difficult to depend on crates from within `proc_macro` due to it being built at the same time as `std`. In addition, a custom arena allocator and symbol store was also added, inspired by those in `rustc_arena` and `rustc_span`. To prevent symbol re-use across multiple invocations of a macro on the same thread, a new range of `Symbol` names are used for each invocation of the macro, and symbols from previous invocations are cleaned-up. In order to keep `Ident` creation efficient, a special ASCII-only case was added to perform ident validation without using RPC for simple identifiers. Full identifier validation couldn't be easily added, as it would require depending on the `rustc_lexer` and `unicode-normalization` crates from within `proc_macro`. Unicode identifiers are validated and normalized using RPC. See the individual commit messages for more details on trade-offs and design decisions behind these patches.
2 parents 96c2df8 + c4acac6 commit c3f3550

File tree

13 files changed

+857
-379
lines changed

13 files changed

+857
-379
lines changed
 

‎compiler/rustc_expand/src/proc_macro_server.rs

+173-229
Large diffs are not rendered by default.
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//! A minimal arena allocator inspired by `rustc_arena::DroplessArena`.
2+
//!
3+
//! This is unfortunately a minimal re-implementation rather than a dependency
4+
//! as it is difficult to depend on crates from within `proc_macro`, due to it
5+
//! being built at the same time as `std`.
6+
7+
use std::cell::{Cell, RefCell};
8+
use std::cmp;
9+
use std::mem::MaybeUninit;
10+
use std::ops::Range;
11+
use std::ptr;
12+
use std::slice;
13+
use std::str;
14+
15+
// The arenas start with PAGE-sized chunks, and then each new chunk is twice as
16+
// big as its predecessor, up until we reach HUGE_PAGE-sized chunks, whereupon
17+
// we stop growing. This scales well, from arenas that are barely used up to
18+
// arenas that are used for 100s of MiBs. Note also that the chosen sizes match
19+
// the usual sizes of pages and huge pages on Linux.
20+
const PAGE: usize = 4096;
21+
const HUGE_PAGE: usize = 2 * 1024 * 1024;
22+
23+
/// A minimal arena allocator inspired by `rustc_arena::DroplessArena`.
24+
///
25+
/// This is unfortunately a complete re-implementation rather than a dependency
26+
/// as it is difficult to depend on crates from within `proc_macro`, due to it
27+
/// being built at the same time as `std`.
28+
///
29+
/// This arena doesn't have support for allocating anything other than byte
30+
/// slices, as that is all that is necessary.
31+
pub(crate) struct Arena {
32+
start: Cell<*mut MaybeUninit<u8>>,
33+
end: Cell<*mut MaybeUninit<u8>>,
34+
chunks: RefCell<Vec<Box<[MaybeUninit<u8>]>>>,
35+
}
36+
37+
impl Arena {
38+
pub(crate) fn new() -> Self {
39+
Arena {
40+
start: Cell::new(ptr::null_mut()),
41+
end: Cell::new(ptr::null_mut()),
42+
chunks: RefCell::new(Vec::new()),
43+
}
44+
}
45+
46+
/// Add a new chunk with at least `additional` free bytes.
47+
#[inline(never)]
48+
#[cold]
49+
fn grow(&self, additional: usize) {
50+
let mut chunks = self.chunks.borrow_mut();
51+
let mut new_cap;
52+
if let Some(last_chunk) = chunks.last_mut() {
53+
// If the previous chunk's len is less than HUGE_PAGE
54+
// bytes, then this chunk will be least double the previous
55+
// chunk's size.
56+
new_cap = last_chunk.len().min(HUGE_PAGE / 2);
57+
new_cap *= 2;
58+
} else {
59+
new_cap = PAGE;
60+
}
61+
// Also ensure that this chunk can fit `additional`.
62+
new_cap = cmp::max(additional, new_cap);
63+
64+
let mut chunk = Box::new_uninit_slice(new_cap);
65+
let Range { start, end } = chunk.as_mut_ptr_range();
66+
self.start.set(start);
67+
self.end.set(end);
68+
chunks.push(chunk);
69+
}
70+
71+
/// Allocates a byte slice with specified size from the current memory
72+
/// chunk. Returns `None` if there is no free space left to satisfy the
73+
/// request.
74+
fn alloc_raw_without_grow(&self, bytes: usize) -> Option<&mut [MaybeUninit<u8>]> {
75+
let start = self.start.get().addr();
76+
let old_end = self.end.get();
77+
let end = old_end.addr();
78+
79+
let new_end = end.checked_sub(bytes)?;
80+
if start <= new_end {
81+
let new_end = old_end.with_addr(new_end);
82+
self.end.set(new_end);
83+
// SAFETY: `bytes` bytes starting at `new_end` were just reserved.
84+
Some(unsafe { slice::from_raw_parts_mut(new_end, bytes) })
85+
} else {
86+
None
87+
}
88+
}
89+
90+
fn alloc_raw(&self, bytes: usize) -> &mut [MaybeUninit<u8>] {
91+
if bytes == 0 {
92+
return &mut [];
93+
}
94+
95+
loop {
96+
if let Some(a) = self.alloc_raw_without_grow(bytes) {
97+
break a;
98+
}
99+
// No free space left. Allocate a new chunk to satisfy the request.
100+
// On failure the grow will panic or abort.
101+
self.grow(bytes);
102+
}
103+
}
104+
105+
pub(crate) fn alloc_str<'a>(&'a self, string: &str) -> &'a mut str {
106+
let alloc = self.alloc_raw(string.len());
107+
let bytes = MaybeUninit::write_slice(alloc, string.as_bytes());
108+
109+
// SAFETY: we convert from `&str` to `&[u8]`, clone it into the arena,
110+
// and immediately convert the clone back to `&str`.
111+
unsafe { str::from_utf8_unchecked_mut(bytes) }
112+
}
113+
}

‎library/proc_macro/src/bridge/client.rs

+9-21
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,11 @@ define_handles! {
175175
'owned:
176176
FreeFunctions,
177177
TokenStream,
178-
Literal,
179178
SourceFile,
180179
MultiSpan,
181180
Diagnostic,
182181

183182
'interned:
184-
Ident,
185183
Span,
186184
}
187185

@@ -197,25 +195,6 @@ impl Clone for TokenStream {
197195
}
198196
}
199197

200-
impl Clone for Literal {
201-
fn clone(&self) -> Self {
202-
self.clone()
203-
}
204-
}
205-
206-
impl fmt::Debug for Literal {
207-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208-
f.debug_struct("Literal")
209-
// format the kind without quotes, as in `kind: Float`
210-
.field("kind", &format_args!("{}", &self.debug_kind()))
211-
.field("symbol", &self.symbol())
212-
// format `Some("...")` on one line even in {:#?} mode
213-
.field("suffix", &format_args!("{:?}", &self.suffix()))
214-
.field("span", &self.span())
215-
.finish()
216-
}
217-
}
218-
219198
impl Clone for SourceFile {
220199
fn clone(&self) -> Self {
221200
self.clone()
@@ -242,6 +221,8 @@ impl fmt::Debug for Span {
242221
}
243222
}
244223

224+
pub(crate) use super::symbol::Symbol;
225+
245226
macro_rules! define_client_side {
246227
($($name:ident {
247228
$(fn $method:ident($($arg:ident: $arg_ty:ty),* $(,)?) $(-> $ret_ty:ty)*;)*
@@ -405,6 +386,9 @@ fn run_client<A: for<'a, 's> DecodeMut<'a, 's, ()>, R: Encode<()>>(
405386
panic::catch_unwind(panic::AssertUnwindSafe(|| {
406387
maybe_install_panic_hook(force_show_panics);
407388

389+
// Make sure the symbol store is empty before decoding inputs.
390+
Symbol::invalidate_all();
391+
408392
let reader = &mut &buf[..];
409393
let (globals, input) = <(ExpnGlobals<Span>, A)>::decode(reader, &mut ());
410394

@@ -438,6 +422,10 @@ fn run_client<A: for<'a, 's> DecodeMut<'a, 's, ()>, R: Encode<()>>(
438422
buf.clear();
439423
Err::<(), _>(e).encode(&mut buf, &mut ());
440424
});
425+
426+
// Now that a response has been serialized, invalidate all symbols
427+
// registered with the interner.
428+
Symbol::invalidate_all();
441429
buf
442430
}
443431

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//! This is a copy of the `rustc_hash` crate, adapted to work as a module.
2+
//!
3+
//! If in the future it becomes more reasonable to add dependencies to
4+
//! `proc_macro`, this module should be removed and replaced with a dependency
5+
//! on the `rustc_hash` crate.
6+
7+
use std::collections::HashMap;
8+
use std::convert::TryInto;
9+
use std::default::Default;
10+
use std::hash::BuildHasherDefault;
11+
use std::hash::Hasher;
12+
use std::mem::size_of;
13+
use std::ops::BitXor;
14+
15+
/// Type alias for a hashmap using the `fx` hash algorithm.
16+
pub type FxHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
17+
18+
/// A speedy hash algorithm for use within rustc. The hashmap in liballoc
19+
/// by default uses SipHash which isn't quite as speedy as we want. In the
20+
/// compiler we're not really worried about DOS attempts, so we use a fast
21+
/// non-cryptographic hash.
22+
///
23+
/// This is the same as the algorithm used by Firefox -- which is a homespun
24+
/// one not based on any widely-known algorithm -- though modified to produce
25+
/// 64-bit hash values instead of 32-bit hash values. It consistently
26+
/// out-performs an FNV-based hash within rustc itself -- the collision rate is
27+
/// similar or slightly worse than FNV, but the speed of the hash function
28+
/// itself is much higher because it works on up to 8 bytes at a time.
29+
pub struct FxHasher {
30+
hash: usize,
31+
}
32+
33+
#[cfg(target_pointer_width = "32")]
34+
const K: usize = 0x9e3779b9;
35+
#[cfg(target_pointer_width = "64")]
36+
const K: usize = 0x517cc1b727220a95;
37+
38+
impl Default for FxHasher {
39+
#[inline]
40+
fn default() -> FxHasher {
41+
FxHasher { hash: 0 }
42+
}
43+
}
44+
45+
impl FxHasher {
46+
#[inline]
47+
fn add_to_hash(&mut self, i: usize) {
48+
self.hash = self.hash.rotate_left(5).bitxor(i).wrapping_mul(K);
49+
}
50+
}
51+
52+
impl Hasher for FxHasher {
53+
#[inline]
54+
fn write(&mut self, mut bytes: &[u8]) {
55+
#[cfg(target_pointer_width = "32")]
56+
let read_usize = |bytes: &[u8]| u32::from_ne_bytes(bytes[..4].try_into().unwrap());
57+
#[cfg(target_pointer_width = "64")]
58+
let read_usize = |bytes: &[u8]| u64::from_ne_bytes(bytes[..8].try_into().unwrap());
59+
60+
let mut hash = FxHasher { hash: self.hash };
61+
assert!(size_of::<usize>() <= 8);
62+
while bytes.len() >= size_of::<usize>() {
63+
hash.add_to_hash(read_usize(bytes) as usize);
64+
bytes = &bytes[size_of::<usize>()..];
65+
}
66+
if (size_of::<usize>() > 4) && (bytes.len() >= 4) {
67+
hash.add_to_hash(u32::from_ne_bytes(bytes[..4].try_into().unwrap()) as usize);
68+
bytes = &bytes[4..];
69+
}
70+
if (size_of::<usize>() > 2) && bytes.len() >= 2 {
71+
hash.add_to_hash(u16::from_ne_bytes(bytes[..2].try_into().unwrap()) as usize);
72+
bytes = &bytes[2..];
73+
}
74+
if (size_of::<usize>() > 1) && bytes.len() >= 1 {
75+
hash.add_to_hash(bytes[0] as usize);
76+
}
77+
self.hash = hash.hash;
78+
}
79+
80+
#[inline]
81+
fn write_u8(&mut self, i: u8) {
82+
self.add_to_hash(i as usize);
83+
}
84+
85+
#[inline]
86+
fn write_u16(&mut self, i: u16) {
87+
self.add_to_hash(i as usize);
88+
}
89+
90+
#[inline]
91+
fn write_u32(&mut self, i: u32) {
92+
self.add_to_hash(i as usize);
93+
}
94+
95+
#[cfg(target_pointer_width = "32")]
96+
#[inline]
97+
fn write_u64(&mut self, i: u64) {
98+
self.add_to_hash(i as usize);
99+
self.add_to_hash((i >> 32) as usize);
100+
}
101+
102+
#[cfg(target_pointer_width = "64")]
103+
#[inline]
104+
fn write_u64(&mut self, i: u64) {
105+
self.add_to_hash(i as usize);
106+
}
107+
108+
#[inline]
109+
fn write_usize(&mut self, i: usize) {
110+
self.add_to_hash(i);
111+
}
112+
113+
#[inline]
114+
fn finish(&self) -> u64 {
115+
self.hash as u64
116+
}
117+
}

‎library/proc_macro/src/bridge/handle.rs

+6-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! Server-side handles and storage for per-handle data.
22
3-
use std::collections::{BTreeMap, HashMap};
4-
use std::hash::{BuildHasher, Hash};
3+
use std::collections::BTreeMap;
4+
use std::hash::Hash;
55
use std::num::NonZeroU32;
66
use std::ops::{Index, IndexMut};
77
use std::sync::atomic::{AtomicUsize, Ordering};
88

9+
use super::fxhash::FxHashMap;
10+
911
pub(super) type Handle = NonZeroU32;
1012

1113
/// A store that associates values of type `T` with numeric handles. A value can
@@ -51,31 +53,15 @@ impl<T> IndexMut<Handle> for OwnedStore<T> {
5153
}
5254
}
5355

54-
// HACK(eddyb) deterministic `std::collections::hash_map::RandomState` replacement
55-
// that doesn't require adding any dependencies to `proc_macro` (like `rustc-hash`).
56-
#[derive(Clone)]
57-
struct NonRandomState;
58-
59-
impl BuildHasher for NonRandomState {
60-
type Hasher = std::collections::hash_map::DefaultHasher;
61-
#[inline]
62-
fn build_hasher(&self) -> Self::Hasher {
63-
Self::Hasher::new()
64-
}
65-
}
66-
6756
/// Like `OwnedStore`, but avoids storing any value more than once.
6857
pub(super) struct InternedStore<T: 'static> {
6958
owned: OwnedStore<T>,
70-
interner: HashMap<T, Handle, NonRandomState>,
59+
interner: FxHashMap<T, Handle>,
7160
}
7261

7362
impl<T: Copy + Eq + Hash> InternedStore<T> {
7463
pub(super) fn new(counter: &'static AtomicUsize) -> Self {
75-
InternedStore {
76-
owned: OwnedStore::new(counter),
77-
interner: HashMap::with_hasher(NonRandomState),
78-
}
64+
InternedStore { owned: OwnedStore::new(counter), interner: FxHashMap::default() }
7965
}
8066

8167
pub(super) fn alloc(&mut self, x: T) -> Handle {

0 commit comments

Comments
 (0)
Please sign in to comment.