Auto merge of #136401 - Mark-Simulacrum:lockfree-as-str, r= · rust-lang/rust@bad4b51 (original) (raw)

`@@ -2,11 +2,13 @@

`

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::alloc::Layout;

`

``

6

`+

use std::hash::{BuildHasher, BuildHasherDefault, Hash, Hasher};

`

``

7

`+

use std::sync::Mutex;

`

``

8

`+

use std::sync::atomic::{AtomicPtr, 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

`};

`

`@@ -2460,18 +2462,9 @@ impl Symbol {

`

2460

2462

`with_session_globals(|session_globals| session_globals.symbol_interner.intern(string))

`

2461

2463

`}

`

2462

2464

``

2463

``

`-

/// Access the underlying string. This is a slowish operation because it

`

2464

``

`-

/// requires locking the symbol interner.

`

2465

``

`-

///

`

2466

``

`-

/// Note that the lifetime of the return value is a lie. It's not the same

`

2467

``

`` -

/// as &self, but actually tied to the lifetime of the underlying

``

2468

``

`-

/// interner. Interners are long-lived, and there are very few of them, and

`

2469

``

`-

/// this function is typically used for short-lived things, so in practice

`

2470

``

`-

/// it works out ok.

`

``

2465

`+

/// Access the underlying string.

`

2471

2466

`pub fn as_str(&self) -> &str {

`

2472

``

`-

with_session_globals(|session_globals| unsafe {

`

2473

``

`-

std::mem::transmute::<&str, &str>(session_globals.symbol_interner.get(*self))

`

2474

``

`-

})

`

``

2467

`+

with_session_globals(|session_globals| session_globals.symbol_interner.get(*self))

`

2475

2468

`}

`

2476

2469

``

2477

2470

`pub fn as_u32(self) -> u32 {

`

`@@ -2526,53 +2519,137 @@ impl StableCompare for Symbol {

`

2526

2519

`}

`

2527

2520

`}

`

2528

2521

``

2529

``

`-

pub(crate) struct Interner(Lock);

`

``

2522

`+

// This is never de-initialized and stores interned &str in static storage.

`

``

2523

`+

// Each str is stored length-prefixed (u32), and we allow for random-access indexing with a u32

`

``

2524

`+

// index by direct lookup in the arena. Indices <2^16 are stored in a separate structure (they are

`

``

2525

`+

// pre-allocated at dense addresses so we can't use the same lockless O(1) hack for them).

`

``

2526

`+

static GLOBAL_ARENA: std::sync::LazyLock =

`

``

2527

`+

std::sync::LazyLock::new(|| StringArena::new());

`

``

2528

+

``

2529

`+

struct StringArena {

`

``

2530

`+

base: *mut u8,

`

``

2531

`+

end: *mut u8,

`

``

2532

`+

write_at: AtomicPtr,

`

``

2533

`` +

// this guards writes to write_at, but not reads, which proceed lock-free. write_at is only moved

``

``

2534

`+

// forward so this ends up being safe.

`

``

2535

`+

writer: std::sync::Mutex<()>,

`

``

2536

`+

}

`

``

2537

+

``

2538

`+

unsafe impl Sync for StringArena {}

`

``

2539

`+

unsafe impl Send for StringArena {}

`

``

2540

+

``

2541

`+

impl StringArena {

`

``

2542

`+

fn new() -> Self {

`

``

2543

`+

unsafe {

`

``

2544

`+

let layout =

`

``

2545

`+

Layout::from_size_align(u32::MAX as usize, std::mem::align_of::()).unwrap();

`

``

2546

`+

let allocation = std::alloc::alloc_zeroed(layout);

`

``

2547

`+

if allocation.is_null() {

`

``

2548

`+

std::alloc::handle_alloc_error(layout)

`

``

2549

`+

}

`

``

2550

`+

StringArena {

`

``

2551

`+

base: allocation,

`

``

2552

`+

end: allocation.add(layout.size()),

`

``

2553

`+

// Reserve 2^16 u32 indices -- these will be used for pre-filled interning where we

`

``

2554

`+

// have a dense SymbolIndex space. We could make this exact but it doesn't really

`

``

2555

`+

// matter for this initial test anyway.

`

``

2556

`+

write_at: AtomicPtr::new(allocation.add(u16::MAX as usize)),

`

``

2557

`+

writer: Mutex::new(()),

`

``

2558

`+

}

`

``

2559

`+

}

`

``

2560

`+

}

`

``

2561

+

``

2562

`+

fn alloc(&self, s: &str) -> u32 {

`

``

2563

`+

unsafe {

`

``

2564

`+

let _guard = self.writer.lock().unwrap();

`

``

2565

`+

// Allocate a chunk of the region, and fill it with the &str's length and bytes.

`

``

2566

`+

let dst = self.write_at.load(Ordering::Acquire);

`

``

2567

`+

// Assert we're in-bounds.

`

``

2568

`+

assert!(

`

``

2569

`+

dst.addr().checked_add(4).unwrap().checked_add(s.len()).unwrap() < self.end.addr()

`

``

2570

`+

);

`

``

2571

`+

dst.cast::().write_unaligned(s.len().try_into().unwrap());

`

``

2572

`+

dst.add(4).copy_from_nonoverlapping(s.as_ptr(), s.len());

`

``

2573

+

``

2574

`+

// Semantically this releases the memory for readers.

`

``

2575

`+

self.write_at.store(dst.add(4 + s.len()), Ordering::Release);

`

``

2576

+

``

2577

`+

dst.byte_offset_from(self.base).try_into().unwrap()

`

``

2578

`+

}

`

``

2579

`+

}

`

``

2580

+

``

2581

`+

fn get(&self, idx: u32) -> &str {

`

``

2582

`` +

// SAFETY: write_at is only updated after writing to a given region, so we never have

``

``

2583

`+

// concurrent writes to this memory. It's initialized at allocation (alloc_zeroed) and we

`

``

2584

`+

// only write initialized bytes to it, so there's also never any uninit bytes in the region.

`

``

2585

`+

let region = unsafe {

`

``

2586

`+

let end = self.write_at.load(Ordering::Acquire);

`

``

2587

`+

std::slice::from_raw_parts(self.base, end.offset_from(self.base) as usize)

`

``

2588

`+

};

`

``

2589

+

``

2590

`+

let len = u32::from_ne_bytes(region[idx as usize..idx as usize + 4].try_into().unwrap());

`

``

2591

`+

let data = &region[idx as usize + 4..][..len as usize];

`

``

2592

+

``

2593

`` +

// SAFETY: We (in theory) could be passed a random idx into the memory region, so we need

``

``

2594

`+

// to re-check that the region is actually UTF-8 before returning. If it is, then it is safe

`

``

2595

`+

// to return &str, though it might not be a useful &str due to having near-random

`

``

2596

`+

// contents.

`

``

2597

`+

std::str::from_utf8(data).unwrap()

`

``

2598

`+

}

`

``

2599

`+

}

`

``

2600

+

``

2601

`+

pub(crate) struct Interner(&'static [&'static str], Lock);

`

2530

2602

``

2531

``

`` -

// The &'static strs in this type actually point into the arena.

``

2532

``

`-

//

`

2533

``

`-

// This type is private to prevent accidentally constructing more than one

`

2534

``

`` -

// Interner on the same thread, which makes it easy to mix up Symbols

``

2535

``

`` -

// between Interners.

``

2536

2603

`struct InternerInner {

`

2537

``

`-

arena: DroplessArena,

`

2538

``

`-

strings: FxIndexSet<&'static str>,

`

``

2604

`+

strings: hashbrown::HashTable,

`

2539

2605

`}

`

2540

2606

``

2541

2607

`impl Interner {

`

2542

``

`-

fn prefill(init: &[&'static str]) -> Self {

`

2543

``

`-

Interner(Lock::new(InternerInner {

`

2544

``

`-

arena: Default::default(),

`

2545

``

`-

strings: init.iter().copied().collect(),

`

2546

``

`-

}))

`

``

2608

`+

fn prefill(init: &'static [&'static str]) -> Self {

`

``

2609

`+

assert!(init.len() < u16::MAX as usize);

`

``

2610

`+

let mut strings = hashbrown::HashTable::new();

`

``

2611

+

``

2612

`+

for (idx, s) in init.iter().copied().enumerate() {

`

``

2613

`+

let mut hasher = FxHasher::default();

`

``

2614

`+

s.hash(&mut hasher);

`

``

2615

`+

let hash = hasher.finish();

`

``

2616

`+

strings.insert_unique(hash, Symbol::new(idx as u32), |val| {

`

``

2617

`` +

// has to be from init because we haven't yet inserted anything except those.

``

``

2618

`+

BuildHasherDefault::::default().hash_one(init[val.0.index()])

`

``

2619

`+

});

`

``

2620

`+

}

`

``

2621

+

``

2622

`+

Interner(init, Lock::new(InternerInner { strings }))

`

2547

2623

`}

`

2548

2624

``

2549

2625

`#[inline]

`

2550

2626

`fn intern(&self, string: &str) -> Symbol {

`

2551

``

`-

let mut inner = self.0.lock();

`

2552

``

`-

if let Some(idx) = inner.strings.get_index_of(string) {

`

2553

``

`-

return Symbol::new(idx as u32);

`

``

2627

`+

let hash = BuildHasherDefault::::default().hash_one(string);

`

``

2628

`+

let mut inner = self.1.lock();

`

``

2629

`+

match inner.strings.find_entry(hash, |v| self.get(*v) == string) {

`

``

2630

`+

Ok(e) => return *e.get(),

`

``

2631

`+

Err(e) => {

`

``

2632

`+

let idx = GLOBAL_ARENA.alloc(string);

`

``

2633

`+

let res = Symbol::new(idx as u32);

`

``

2634

+

``

2635

`+

e.into_table().insert_unique(hash, res, |val| {

`

``

2636

`+

BuildHasherDefault::::default().hash_one(self.get(*val))

`

``

2637

`+

});

`

``

2638

+

``

2639

`+

res

`

``

2640

`+

}

`

2554

2641

`}

`

2555

``

-

2556

``

`-

let string: &str = inner.arena.alloc_str(string);

`

2557

``

-

2558

``

`` -

// SAFETY: we can extend the arena allocation to 'static because we

``

2559

``

`-

// only access these while the arena is still alive.

`

2560

``

`-

let string: &'static str = unsafe { &*(string as *const str) };

`

2561

``

-

2562

``

`` -

// This second hash table lookup can be avoided by using RawEntryMut,

``

2563

``

`-

// but this code path isn't hot enough for it to be worth it. See

`

2564

``

`-

// #91445 for details.

`

2565

``

`-

let (idx, is_new) = inner.strings.insert_full(string);

`

2566

``

`-

debug_assert!(is_new); // due to the get_index_of check above

`

2567

``

-

2568

``

`-

Symbol::new(idx as u32)

`

2569

2642

`}

`

2570

2643

``

2571

2644

`/// Get the symbol as a string.

`

2572

2645

`///

`

2573

2646

`` /// [Symbol::as_str()] should be used in preference to this function.

``

2574

``

`-

fn get(&self, symbol: Symbol) -> &str {

`

2575

``

`-

self.0.lock().strings.get_index(symbol.0.as_usize()).unwrap()

`

``

2647

`+

fn get(&self, symbol: Symbol) -> &'static str {

`

``

2648

`+

if symbol.0.index() < u16::MAX as usize {

`

``

2649

`+

self.0[symbol.0.index()]

`

``

2650

`+

} else {

`

``

2651

`+

GLOBAL_ARENA.get(symbol.0.as_u32())

`

``

2652

`+

}

`

2576

2653

`}

`

2577

2654

`}

`

2578

2655

``