|
2 | 2 | //! allows bidirectional lookup; i.e., given a value, one can easily find the
|
3 | 3 | //! type, and vice versa.
|
4 | 4 |
|
5 |
| -use std::hash::{Hash, Hasher}; |
| 5 | +use std::cell::SyncUnsafeCell; |
| 6 | +use std::hash::{BuildHasher, BuildHasherDefault, Hash, Hasher}; |
| 7 | +use std::sync::LazyLock; |
| 8 | +use std::sync::atomic::{AtomicU32, Ordering}; |
6 | 9 | use std::{fmt, str};
|
7 | 10 |
|
8 |
| -use rustc_arena::DroplessArena; |
9 |
| -use rustc_data_structures::fx::FxIndexSet; |
| 11 | +use rustc_data_structures::fx::FxHasher; |
10 | 12 | use rustc_data_structures::stable_hasher::{
|
11 | 13 | HashStable, StableCompare, StableHasher, ToStableHashKey,
|
12 | 14 | };
|
@@ -2521,18 +2523,9 @@ impl Symbol {
|
2521 | 2523 | with_session_globals(|session_globals| session_globals.symbol_interner.intern(string))
|
2522 | 2524 | }
|
2523 | 2525 |
|
2524 |
| - /// Access the underlying string. This is a slowish operation because it |
2525 |
| - /// requires locking the symbol interner. |
2526 |
| - /// |
2527 |
| - /// Note that the lifetime of the return value is a lie. It's not the same |
2528 |
| - /// as `&self`, but actually tied to the lifetime of the underlying |
2529 |
| - /// interner. Interners are long-lived, and there are very few of them, and |
2530 |
| - /// this function is typically used for short-lived things, so in practice |
2531 |
| - /// it works out ok. |
| 2526 | + /// Access the underlying string. |
2532 | 2527 | pub fn as_str(&self) -> &str {
|
2533 |
| - with_session_globals(|session_globals| unsafe { |
2534 |
| - std::mem::transmute::<&str, &str>(session_globals.symbol_interner.get(*self)) |
2535 |
| - }) |
| 2528 | + with_session_globals(|session_globals| session_globals.symbol_interner.get(*self)) |
2536 | 2529 | }
|
2537 | 2530 |
|
2538 | 2531 | pub fn as_u32(self) -> u32 {
|
@@ -2587,53 +2580,185 @@ impl StableCompare for Symbol {
|
2587 | 2580 | }
|
2588 | 2581 | }
|
2589 | 2582 |
|
2590 |
| -pub(crate) struct Interner(Lock<InternerInner>); |
| 2583 | +// This is never de-initialized and stores interned &str in static storage. |
| 2584 | +// Each str is stored length-prefixed (u32), and we allow for random-access indexing with a u32 |
| 2585 | +// index by direct lookup in the arena. Indices <2^16 are stored in a separate structure (they are |
| 2586 | +// pre-allocated at dense addresses so we can't use the same lockless O(1) hack for them). |
| 2587 | +static GLOBAL_ARENA: StringArena = StringArena::new(); |
2591 | 2588 |
|
2592 |
| -// The `&'static str`s in this type actually point into the arena. |
2593 |
| -// |
2594 |
| -// This type is private to prevent accidentally constructing more than one |
2595 |
| -// `Interner` on the same thread, which makes it easy to mix up `Symbol`s |
2596 |
| -// between `Interner`s. |
2597 |
| -struct InternerInner { |
2598 |
| - arena: DroplessArena, |
2599 |
| - strings: FxIndexSet<&'static str>, |
| 2589 | +const CHUNK_SIZE: usize = 4 * 1024 * 1024; |
| 2590 | +const CHUNKS: usize = (u32::MAX as usize).div_ceil(CHUNK_SIZE); |
| 2591 | + |
| 2592 | +struct StringChunk { |
| 2593 | + array: LazyLock<Box<[SyncUnsafeCell<u8>; CHUNK_SIZE]>>, |
2600 | 2594 | }
|
2601 | 2595 |
|
2602 |
| -impl Interner { |
2603 |
| - fn prefill(init: &[&'static str]) -> Self { |
2604 |
| - Interner(Lock::new(InternerInner { |
2605 |
| - arena: Default::default(), |
2606 |
| - strings: init.iter().copied().collect(), |
2607 |
| - })) |
| 2596 | +impl StringChunk { |
| 2597 | + const fn new() -> Self { |
| 2598 | + Self { |
| 2599 | + array: LazyLock::new(|| unsafe { |
| 2600 | + // SAFETY: Zero-init'd UnsafeCell<u8> is initialized and has no other invariants to |
| 2601 | + // worry about. |
| 2602 | + Box::new_zeroed().assume_init() |
| 2603 | + }), |
| 2604 | + } |
2608 | 2605 | }
|
| 2606 | +} |
2609 | 2607 |
|
2610 |
| - #[inline] |
2611 |
| - fn intern(&self, string: &str) -> Symbol { |
2612 |
| - let mut inner = self.0.lock(); |
2613 |
| - if let Some(idx) = inner.strings.get_index_of(string) { |
2614 |
| - return Symbol::new(idx as u32); |
| 2608 | +struct StringArena { |
| 2609 | + chunks: [StringChunk; CHUNKS], |
| 2610 | + next_start: AtomicU32, |
| 2611 | + interned: elsa::sync::LockFreeFrozenVec<InternedString>, |
| 2612 | +} |
| 2613 | + |
| 2614 | +#[derive(Copy, Clone)] |
| 2615 | +struct InternedString { |
| 2616 | + start: u32, |
| 2617 | + length: u32, |
| 2618 | +} |
| 2619 | + |
| 2620 | +impl StringArena { |
| 2621 | + const fn new() -> Self { |
| 2622 | + StringArena { |
| 2623 | + chunks: [const { StringChunk::new() }; CHUNKS], |
| 2624 | + next_start: AtomicU32::new(0), |
| 2625 | + interned: elsa::sync::LockFreeFrozenVec::new(), |
2615 | 2626 | }
|
| 2627 | + } |
2616 | 2628 |
|
2617 |
| - let string: &str = inner.arena.alloc_str(string); |
| 2629 | + fn next(previous: u32, length: u32) -> u32 { |
| 2630 | + let end = previous.checked_add(length).unwrap(); |
| 2631 | + if previous / CHUNK_SIZE as u32 == end / CHUNK_SIZE as u32 { |
| 2632 | + end |
| 2633 | + } else { |
| 2634 | + // If we don't fit in the previous chunk, bump to the start of the next chunk, and set |
| 2635 | + // length to the end. |
| 2636 | + previous.next_multiple_of(CHUNK_SIZE as u32) + length |
| 2637 | + } |
| 2638 | + } |
2618 | 2639 |
|
2619 |
| - // SAFETY: we can extend the arena allocation to `'static` because we |
2620 |
| - // only access these while the arena is still alive. |
2621 |
| - let string: &'static str = unsafe { &*(string as *const str) }; |
| 2640 | + /// Copy the passed &str into the arena. Returns an index that can be passed to `get` to |
| 2641 | + /// retrieve the &str. |
| 2642 | + /// |
| 2643 | + /// u32 is guaranteed to be at least u16::MAX. |
| 2644 | + fn alloc(&self, s: &str) -> u32 { |
| 2645 | + let len = u32::try_from(s.len()).unwrap(); |
| 2646 | + assert!(len < CHUNK_SIZE as u32); |
| 2647 | + |
| 2648 | + let previous = self |
| 2649 | + .next_start |
| 2650 | + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |previous| { |
| 2651 | + Some(Self::next(previous, len)) |
| 2652 | + }) |
| 2653 | + .unwrap(); |
| 2654 | + let end = Self::next(previous, len); |
| 2655 | + let start = end - len; |
| 2656 | + |
| 2657 | + let chunk = LazyLock::force(&self.chunks[start as usize / CHUNK_SIZE].array); |
| 2658 | + let offset = start as usize % CHUNK_SIZE; |
| 2659 | + |
| 2660 | + // SAFETY: |
| 2661 | + // |
| 2662 | + // * `next_start` only increases, and always uniquely allocates `len` bytes. No one can read |
| 2663 | + // this memory yet as we haven't pushed yet to `interned`. |
| 2664 | + // * all chunks are zero-init'd at allocation: no uninitialized memory here. |
| 2665 | + let dst = unsafe { |
| 2666 | + std::slice::from_raw_parts_mut( |
| 2667 | + chunk.as_ptr().cast::<u8>().add(offset).cast_mut(), |
| 2668 | + s.len(), |
| 2669 | + ) |
| 2670 | + }; |
| 2671 | + dst.copy_from_slice(s.as_bytes()); |
| 2672 | + |
| 2673 | + let idx = self.interned.push(InternedString { start, length: len }); |
| 2674 | + |
| 2675 | + idx.try_into().unwrap() |
| 2676 | + } |
| 2677 | + |
| 2678 | + /// Get the allocated string at the passed index. |
| 2679 | + /// |
| 2680 | + /// Note that this **does not** check that the passed index is actually an index returned by |
| 2681 | + /// `alloc`. |
| 2682 | + fn get(&self, idx: u32) -> &str { |
| 2683 | + let interned = self.interned.get(idx as usize).unwrap_or_else(|| { |
| 2684 | + panic!("non-interned symbol index: {idx}"); |
| 2685 | + }); |
| 2686 | + |
| 2687 | + let Some(chunk) = LazyLock::get(&self.chunks[interned.start as usize / CHUNK_SIZE].array) |
| 2688 | + else { |
| 2689 | + // chunk must be initialized because `interned` points to the chunk. |
| 2690 | + unsafe { std::hint::unreachable_unchecked() } |
| 2691 | + }; |
| 2692 | + let offset = interned.start as usize % CHUNK_SIZE; |
| 2693 | + |
| 2694 | + // We write the string into this memory range prior to pushing into `interned`, so this is |
| 2695 | + // guaranteed UTF-8 and initialized. `next_start` is strictly increasing so we never write |
| 2696 | + // twice. |
| 2697 | + unsafe { |
| 2698 | + std::str::from_raw_parts( |
| 2699 | + chunk.as_ptr().add(offset).cast::<u8>(), |
| 2700 | + interned.length as usize, |
| 2701 | + ) |
| 2702 | + } |
| 2703 | + } |
| 2704 | +} |
2622 | 2705 |
|
2623 |
| - // This second hash table lookup can be avoided by using `RawEntryMut`, |
2624 |
| - // but this code path isn't hot enough for it to be worth it. See |
2625 |
| - // #91445 for details. |
2626 |
| - let (idx, is_new) = inner.strings.insert_full(string); |
2627 |
| - debug_assert!(is_new); // due to the get_index_of check above |
| 2706 | +pub(crate) struct Interner(&'static [&'static str], Lock<InternerInner>); |
2628 | 2707 |
|
2629 |
| - Symbol::new(idx as u32) |
| 2708 | +struct InternerInner { |
| 2709 | + strings: hashbrown::HashTable<Symbol>, |
| 2710 | +} |
| 2711 | + |
| 2712 | +impl Interner { |
| 2713 | + fn prefill(init: &'static [&'static str]) -> Self { |
| 2714 | + assert!(init.len() < u16::MAX as usize); |
| 2715 | + let mut strings = hashbrown::HashTable::with_capacity(32_000); |
| 2716 | + |
| 2717 | + for (idx, s) in init.iter().copied().enumerate() { |
| 2718 | + let mut hasher = FxHasher::default(); |
| 2719 | + s.hash(&mut hasher); |
| 2720 | + let hash = hasher.finish(); |
| 2721 | + strings.insert_unique(hash, Symbol::new(idx as u32), |val| { |
| 2722 | + // has to be from `init` because we haven't yet inserted anything except those. |
| 2723 | + BuildHasherDefault::<FxHasher>::default().hash_one(init[val.0.index()]) |
| 2724 | + }); |
| 2725 | + } |
| 2726 | + |
| 2727 | + Interner(init, Lock::new(InternerInner { strings })) |
| 2728 | + } |
| 2729 | + |
| 2730 | + #[inline] |
| 2731 | + fn intern(&self, string: &str) -> Symbol { |
| 2732 | + let hash = BuildHasherDefault::<FxHasher>::default().hash_one(string); |
| 2733 | + let mut inner = self.1.lock(); |
| 2734 | + match inner.strings.find_entry(hash, |v| self.get(*v) == string) { |
| 2735 | + Ok(e) => return *e.get(), |
| 2736 | + Err(e) => { |
| 2737 | + let idx = GLOBAL_ARENA.alloc(string); |
| 2738 | + // Reserve 2^16 u32 indices -- these will be used for pre-filled interning where we |
| 2739 | + // have a dense SymbolIndex space. We could make this exact but it doesn't really |
| 2740 | + // matter in practice, we won't run out of symbol space. |
| 2741 | + let idx = u32::from(u16::MAX).checked_add(idx).unwrap(); |
| 2742 | + let res = Symbol::new(idx as u32); |
| 2743 | + |
| 2744 | + e.into_table().insert_unique(hash, res, |val| { |
| 2745 | + BuildHasherDefault::<FxHasher>::default().hash_one(self.get(*val)) |
| 2746 | + }); |
| 2747 | + |
| 2748 | + res |
| 2749 | + } |
| 2750 | + } |
2630 | 2751 | }
|
2631 | 2752 |
|
2632 | 2753 | /// Get the symbol as a string.
|
2633 | 2754 | ///
|
2634 | 2755 | /// [`Symbol::as_str()`] should be used in preference to this function.
|
2635 |
| - fn get(&self, symbol: Symbol) -> &str { |
2636 |
| - self.0.lock().strings.get_index(symbol.0.as_usize()).unwrap() |
| 2756 | + fn get(&self, symbol: Symbol) -> &'static str { |
| 2757 | + if let Some(interned) = symbol.0.as_u32().checked_sub(u32::from(u16::MAX)) { |
| 2758 | + GLOBAL_ARENA.get(interned) |
| 2759 | + } else { |
| 2760 | + self.0[symbol.0.index()] |
| 2761 | + } |
2637 | 2762 | }
|
2638 | 2763 | }
|
2639 | 2764 |
|
|
0 commit comments