concurrent_interner/concurrent_interner.rs
1//! For high-level documentation, see the [Readme](https://github.com/typesanitizer/concurrent-interner).
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7use dashmap::mapref::entry::Entry;
8use dashmap::DashMap;
9
10use std::fmt::Debug;
11use std::hash::{BuildHasher, Hash, Hasher};
12use std::mem::ManuallyDrop;
13use std::num::NonZeroU8;
14use std::sync::Mutex;
15
16#[cfg(feature = "_serial")]
17pub mod serial;
18
19//----------------------------------------------------------------------------
20// Public API - ConcurrentInterner, type definition
21//
22// See [REF NOTE: interner-design] for higher-level discussion.
23
24/// A thread-safe string interner.
25///
26/// The APIs are based on the assumption that you will create one
27/// [`ConcurrentInterner`], intern some strings to get some [`IStr`]s,
28/// and then potentially try to obtain the strings back from the same interner.
29/// Using the [`IStr`] from one interner to obtain the data from another
30/// interner may lead to incorrect results or a panic. It will not lead
31/// to memory unsafety.
32///
33/// See also: [`ConcurrentInternerMember`] and [`ConcurrentInterner::get_member()`].
34///
35/// The `RS` generic parameter should be substituted with a `RandomState` type.
36/// For example, in the context of a compiler, you could use:
37/// - [`ahash::RandomState`](https://docs.rs/ahash/latest/ahash/struct.RandomState.html).
38/// - [`rustc_hash::FxHasher`](https://docs.rs/rustc-hash/latest/rustc_hash/struct.FxHasher.html)
39/// wrapped by a [`BuildHasherDefault`](https://doc.rust-lang.org/std/hash/struct.BuildHasherDefault.html).
40///
41/// In some early testing, FxHash sometimes turns out to be a smidge faster
42/// for a workload involving code, but honestly it is hard to tell the difference.
43pub struct ConcurrentInterner<RS: Send + Sync + Clone + BuildHasher> {
44 /// Thread-safe hash-table for mapping a small string -> IStr.
45 indices: ConcurrentInternerMap<RS>,
46
47 /// Storage arenas for different interner members.
48 ///
49 /// The number of arenas is fixed at initialization time.
50 storage: Mutex<ConcurrentInternerStorage>,
51}
52
53//------------------------------------------------------------------
54// Helper type definition
55
56type ConcurrentInternerMap<RS> = DashMap<SSOStringRef, IStr, RS>;
57
58//------------------------------------------------------------------
59// Trait implementations
60
61impl<RS: Send + Sync + Clone + BuildHasher> std::fmt::Debug for ConcurrentInterner<RS> {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 let mut dbg_map = f.debug_map();
64 for kvref in self.indices.iter() {
65 let key_bytes = unsafe { kvref.key().as_bytes() };
66 let key_str = std::str::from_utf8(key_bytes).expect("invariant violation: invalid utf-8 in storage");
67 dbg_map.entry(&key_str, kvref.value());
68 }
69 dbg_map.finish()
70 }
71}
72
73//------------------------------------------------------------------
74// Constants
75
76impl<RS: Send + Sync + Clone + BuildHasher> ConcurrentInterner<RS> {
77 const LOCK_ACQUIRE_ERROR_MSG: &'static str = "Unable to acquire lock for interner storage.";
78}
79
80//------------------------------------------------------------------
81// Construction API
82
83impl<RS: Send + Sync + Clone + BuildHasher> ConcurrentInterner<RS> {
84 /// Create an interner with one or more arenas.
85 ///
86 /// In most cases, the number of arenas will be equal to the number of
87 /// threads simultaneously using the [`ConcurrentInterner`].
88 /// You should perform some benchmarking for the right `arena_count`
89 /// value, instead of directly setting it to the number of CPU cores,
90 /// since the backing [`DashMap`] does not scale very well under
91 /// high contention.
92 /// (See [Performance numbers](https://github.com/typesanitizer/concurrent-interner/blob/main/docs/Performance.md)).
93 ///
94 /// If you're not sure what `random_state` to plug in,
95 /// [`Default::default()`] is a reasonable choice.
96 pub fn new(arena_count: NonZeroU8, random_state: RS) -> ConcurrentInterner<RS> {
97 // TODO: Expose another constructor which allows passing in a shard amount,
98 // which can be used by ourselves and clients for cross-interpretation
99 // under MIRI. By default, this does I/O by using the open64 syscall
100 // (to read the number of CPUs), which MIRI doesn't support.
101 ConcurrentInterner {
102 indices: ConcurrentInternerMap::with_capacity_and_hasher(64, random_state),
103 storage: Mutex::new(ConcurrentInternerStorage {
104 handed_out_count: 0,
105 boxes: (0..arena_count.get())
106 .map(|i| Box::new(ConcurrentInternerMemberStorage::new(i)))
107 .collect(),
108 }),
109 }
110 }
111}
112
113//------------------------------------------------------------------
114// Query API
115
116impl<RS: Send + Sync + Clone + BuildHasher> ConcurrentInterner<RS> {
117 /// Get a member of the interner, which can be used to intern strings.
118 ///
119 /// NOTE: This method acquires a lock, so you don't want to call it
120 /// willy-nilly (such as for interning a single string).
121 /// Instead, call this when starting to do interning work
122 /// from each thread.
123 pub fn get_member(&self) -> ConcurrentInternerMember<'_, RS> {
124 let mut lock = self.storage.lock().expect(Self::LOCK_ACQUIRE_ERROR_MSG);
125 let storage = lock
126 .boxes
127 .pop()
128 .expect("All ConcurrentInternerMembers are taken!");
129 lock.handed_out_count += 1;
130 ConcurrentInternerMember {
131 indices: &self.indices,
132 storage: ManuallyDrop::new(storage),
133 owner: self,
134 }
135 }
136
137 /// Read a string from a [`ConcurrentInterner`].
138 ///
139 /// This API is primarily present for convenience and debugging.
140 ///
141 /// If you want to retrieve a large number of strings, you can call
142 /// [`ConcurrentInterner::freeze`]
143 /// followed by [`FrozenInterner::get_str`] to directly copy the
144 /// data out, without an intermediate heap allocation.
145 pub fn get_string(&self, istr: IStr) -> String {
146 debug_assert!(self.is_organized(), "Forgot to call reorganize?");
147 let storage = &self
148 .storage
149 .lock()
150 .expect(Self::LOCK_ACQUIRE_ERROR_MSG)
151 .boxes[istr.arena_id() as usize];
152 storage.get_str(istr).to_string()
153 }
154}
155
156//------------------------------------------------------------------
157// Deconstruction API
158
159impl<RS: Send + Sync + Clone + BuildHasher> ConcurrentInterner<RS> {
160 /// Make an interner read-only, allowing for faster string accesses.
161 pub fn freeze(self) -> FrozenInterner<RS> {
162 debug_assert!(self.is_organized(), "Forgot to call reorganize?");
163 FrozenInterner {
164 indices: self.indices,
165 frozen_storage: self
166 .storage
167 .into_inner()
168 .expect(Self::LOCK_ACQUIRE_ERROR_MSG),
169 }
170 }
171}
172
173//------------------------------------------------------------------
174// Private API
175
176impl<RS: Send + Sync + Clone + BuildHasher> ConcurrentInterner<RS> {
177 /// Helper method for checking that the interner is correctly organized.
178 fn is_organized(&self) -> bool {
179 let storage_vec = self.storage.lock().expect(Self::LOCK_ACQUIRE_ERROR_MSG);
180 return storage_vec
181 .boxes
182 .iter()
183 .enumerate()
184 .all(|(i, member)| (member.arena_id as usize) == i);
185 }
186}
187
188//----------------------------------------------------------------------------
189// Public API - ConcurrentInternerMember type definition
190
191/// A member for an interner, used to intern strings into the interner
192/// from a thread without locking.
193pub struct ConcurrentInternerMember<'i, RS: Send + Sync + Clone + BuildHasher> {
194 indices: &'i ConcurrentInternerMap<RS>,
195 storage: ManuallyDrop<Box<ConcurrentInternerMemberStorage>>,
196 owner: &'i ConcurrentInterner<RS>,
197}
198
199//------------------------------------------------------------------
200// Trait implementations
201
202impl<RS: Send + Sync + Clone + BuildHasher> Drop for ConcurrentInternerMember<'_, RS> {
203 fn drop(&mut self) {
204 let mut guard = self
205 .owner
206 .storage
207 .lock()
208 .expect(ConcurrentInterner::<RS>::LOCK_ACQUIRE_ERROR_MSG);
209 // See discussion in bumpalo-herd's comments.
210 // https://docs.rs/bumpalo-herd/0.1.1/src/bumpalo_herd/lib.rs.html#249
211 let storage = unsafe { ManuallyDrop::take(&mut self.storage) };
212 guard.boxes.push(storage);
213 guard.handed_out_count -= 1;
214 if guard.handed_out_count == 0 {
215 guard.boxes.sort_by_key(|s| s.arena_id);
216 }
217 }
218}
219
220//------------------------------------------------------------------
221// Constants
222
223impl<'i, RS: Send + Sync + Clone + BuildHasher> ConcurrentInternerMember<'i, RS> {
224 const STORAGE_CHUNK_SIZE: usize = ConcurrentInternerMemberStorage::STORAGE_CHUNK_SIZE;
225
226 const CHUNK_SLICE_ERROR_MSG: &'static str =
227 ConcurrentInternerMemberStorage::CHUNK_SLICE_ERROR_MSG;
228}
229
230//------------------------------------------------------------------
231// Modification API
232
233impl<'i, RS: Send + Sync + Clone + BuildHasher> ConcurrentInternerMember<'i, RS> {
234 /// Intern a string slice, returning a unique [`IStr`].
235 ///
236 /// If multiple interners attempt to simultaneously intern the same string,
237 /// it is guaranteed that the returned [`IStr`] values will be equal.
238 pub fn intern(&mut self, s: &str) -> IStr {
239 let sref = SSOStringRef::new(s);
240 if let Some(istr) = self.indices.get(&sref) {
241 return istr.value().clone();
242 }
243
244 let istr: IStr;
245 let potential_new_key: SSOStringRef;
246 let storage = self.storage.as_mut();
247 let composite_index: CompositeIndex;
248 if s.len() <= storage.bytes_left {
249 let chunk_index = storage.chunks.len() - 1;
250 let start = Self::STORAGE_CHUNK_SIZE - storage.bytes_left;
251 let range = start..start + s.len();
252 // Remove .clone() once https://github.com/rust-lang/rfcs/issues/2848 is fixed.
253 storage.chunks[chunk_index][range.clone()].copy_from_slice(s.as_bytes());
254 storage.bytes_left -= s.len();
255 // SAFETY: This is safe because (1) only we have mutable
256 // access to the storage and (2) we literally just copied
257 // a valid UTF-8 slice to that range.
258 potential_new_key = unsafe {
259 SSOStringRef::new_unchecked(
260 &storage.chunks[chunk_index]
261 .get(range.clone())
262 .expect(Self::CHUNK_SLICE_ERROR_MSG),
263 )
264 };
265 debug_assert!(start <= u16::MAX as usize);
266 composite_index =
267 CompositeIndex::new_chunk_index(chunk_index, start as u16, s.len() as u16);
268 } else if s.len() <= Self::STORAGE_CHUNK_SIZE {
269 let chunk_index = storage.chunks.len();
270 storage.chunks.push(Box::new(
271 // Cannot use Self::STORAGE_CHUNK_SIZE due to
272 // https://github.com/rust-lang/rust/issues/89236
273 [0; ConcurrentInternerMemberStorage::STORAGE_CHUNK_SIZE],
274 ));
275 let range = 0..s.len();
276 // Remove .clone() once https://github.com/rust-lang/rfcs/issues/2848 is fixed.
277 storage.chunks[chunk_index][range.clone()].copy_from_slice(s.as_bytes());
278 storage.bytes_left = Self::STORAGE_CHUNK_SIZE - s.len();
279 // SAFETY: This is safe because (1) only we have mutable
280 // access to the storage and (2) we literally just copied
281 // a valid UTF-8 slice to that range.
282 potential_new_key = unsafe {
283 SSOStringRef::new_unchecked(
284 &storage.chunks[chunk_index]
285 .get(range)
286 .expect(Self::CHUNK_SLICE_ERROR_MSG),
287 )
288 };
289 composite_index = CompositeIndex::new_chunk_index(chunk_index, 0, s.len() as u16);
290 } else {
291 let huge_index = storage.huge_strings.len();
292 storage.huge_strings.push(s.to_string());
293 potential_new_key = SSOStringRef::new(storage.huge_strings[huge_index].as_str());
294 composite_index = CompositeIndex::new_huge_index(huge_index);
295 }
296 let arena_local_index = storage.alloc_indices.len();
297 storage.alloc_indices.push(composite_index);
298 istr = IStr::new(storage.arena_id, arena_local_index);
299
300 // It is possible that in the meantime, some other thread allocated
301 // the same string into its local arena, entered it into the table,
302 // and returned the corresponding IStrIndex. If that happened, we
303 // need to use the value it returned to be able to compare IStrs
304 // correctly, and ignore our freshly allocated slice.
305
306 match self.indices.entry(potential_new_key) {
307 // TODO: Make some measurements on memory wastage.
308 Entry::Occupied(o) => {
309 self.storage.wasted_bytes += s.len();
310 return *o.get();
311 }
312 Entry::Vacant(v) => {
313 v.insert(istr);
314 return istr;
315 }
316 }
317 }
318}
319
320//----------------------------------------------------------------------------
321// Public API - IStr type definition
322
323/// An opaque interned string that is guaranteed to be 32 bits in size.
324#[derive(Copy, Clone, PartialEq, Eq, Hash)]
325pub struct IStr {
326 raw: u32,
327}
328
329//------------------------------------------------------------------
330// Trait implementations
331
332impl std::fmt::Debug for IStr {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 f.debug_struct("IStr")
335 .field("arena_id", &self.arena_id())
336 .field("arena_local_id", &self.arena_local_id())
337 .finish()
338 }
339}
340
341//------------------------------------------------------------------
342// Private API
343
344impl IStr {
345 fn new(arena_id: u8, arena_local_index: usize) -> IStr {
346 std::debug_assert!(arena_local_index < (2 << 24));
347 IStr {
348 raw: ((arena_id as u32) << 24) | (arena_local_index as u32),
349 }
350 }
351
352 const fn arena_id(&self) -> u8 {
353 (self.raw >> 24) as u8
354 }
355
356 const fn arena_local_id(&self) -> u32 {
357 (self.raw << 8) >> 8
358 }
359}
360
361//----------------------------------------------------------------------------
362// Internal API - ConcurrentInternerStorage
363
364struct ConcurrentInternerStorage {
365 /// The number of storage boxes that have been vended out.
366 handed_out_count: u8,
367 boxes: Vec<Box<ConcurrentInternerMemberStorage>>,
368}
369
370//----------------------------------------------------------------------------
371// Internal API - InternerMemberStorage
372
373/// Arena type for allocating storage for strings in an interner.
374struct ConcurrentInternerMemberStorage {
375 /// The original index of this storage in the parent Interner.
376 arena_id: u8,
377
378 /// Number of bytes left in the current chunk.
379 bytes_left: usize,
380
381 alloc_indices: Vec<CompositeIndex>,
382
383 /// Storage for strings using fixed-size chunks.
384 ///
385 /// Compared to using an increasing size for chunks, this means
386 /// the amount of wasted (allocated but unused) memory is bounded.
387 chunks: Vec<Box<[u8; Self::STORAGE_CHUNK_SIZE]>>,
388
389 /// Standalone storage for strings bigger than STORAGE_CHUNK_SIZE.
390 huge_strings: Vec<String>,
391
392 /// Number of bytes wasted due to lost races.
393 ///
394 /// See [REF NOTE: interner-overallocate].
395 wasted_bytes: usize,
396}
397
398//------------------------------------------------------------------
399// Constants
400
401impl ConcurrentInternerMemberStorage {
402 // IDEA: Is there a crate which exposes page size constants?
403 // I found https://crates.io/crates/page_size, but that is dynamic.
404
405 // IDEA: Is there a way we can measure experimentally the malloc overhead
406 // across different malloc implementations? It would be nice to
407 // have some tests for that.
408
409 // Save 24 bytes for the malloc implementation on Darwin.
410 // https://github.com/apple/swift/commit/64c670745a5bbf4758aa98e96996a5cf53dac344
411 #[cfg(all(target_arch = "aarch64", any(target_os = "macos", target_os = "ios")))]
412 const STORAGE_CHUNK_SIZE: usize = 16384 - 24;
413
414 // Might as well save 24 bytes elsewhere as well. 🤷🏽
415 #[cfg(not(all(target_arch = "aarch64", any(target_os = "macos", target_os = "ios"))))]
416 const STORAGE_CHUNK_SIZE: usize = 4192 - 24;
417
418 const CHUNK_SLICE_ERROR_MSG: &'static str = "Trying to slice chunk with out-of-bounds indices.";
419}
420
421//------------------------------------------------------------------
422// Construction API
423
424impl ConcurrentInternerMemberStorage {
425 fn new(original_index: u8) -> ConcurrentInternerMemberStorage {
426 ConcurrentInternerMemberStorage {
427 arena_id: original_index,
428 bytes_left: Self::STORAGE_CHUNK_SIZE,
429 alloc_indices: vec![],
430 chunks: vec![Box::new([0; Self::STORAGE_CHUNK_SIZE])],
431 huge_strings: vec![],
432 wasted_bytes: 0,
433 }
434 }
435}
436
437//------------------------------------------------------------------
438// Query API
439
440impl ConcurrentInternerMemberStorage {
441 fn get_str(&self, istr: IStr) -> &str {
442 let alloc_id = istr.arena_local_id();
443 let composite_index = self.alloc_indices[alloc_id as usize];
444 if let Some(huge_index) = composite_index.huge_index() {
445 return &self.huge_strings[huge_index as usize];
446 }
447 // Technically, it is possible that we got the interned string from
448 // a different interner entirely, are trying to use that here,
449 // which could lead us to breaking some bytes the wrong way,
450 // but I'm ignoring that for now.
451 let (chunk_index, chunk_start_offset, len) = composite_index
452 .chunk_info()
453 .expect("Unexpected huge string index instead of chunk index.");
454 let (chunk_index, chunk_start_offset, len) = (
455 chunk_index as usize,
456 chunk_start_offset as usize,
457 len as usize,
458 );
459 let bytes = self.chunks[chunk_index]
460 .get(chunk_start_offset..(chunk_start_offset + len))
461 .expect(Self::CHUNK_SLICE_ERROR_MSG);
462 // TODO: Profile if UTF-8 checking here makes a meaningful difference.
463 std::str::from_utf8(bytes).expect("Expected valid UTF-8")
464 }
465}
466
467//----------------------------------------------------------------------------
468// Public API - FrozenInterner type definition
469
470/// Read-only version of [`ConcurrentInterner`].
471///
472/// Allows read access from multiple threads without synchronization
473/// in exchange for not allowing any writes.
474pub struct FrozenInterner<RS: Send + Sync + Clone + BuildHasher> {
475 indices: ConcurrentInternerMap<RS>,
476 frozen_storage: ConcurrentInternerStorage,
477}
478
479//------------------------------------------------------------------
480// Trait implementations
481
482impl<RS: Send + Sync + Clone + BuildHasher> From<ConcurrentInterner<RS>> for FrozenInterner<RS> {
483 /// Identical to [`ConcurrentInterner::freeze`].
484 fn from(interner: ConcurrentInterner<RS>) -> FrozenInterner<RS> {
485 interner.freeze()
486 }
487}
488
489impl<RS: Send + Sync + Clone + BuildHasher> std::fmt::Debug for FrozenInterner<RS> {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 let mut dbg_map = f.debug_map();
492 for kvref in self.indices.iter() {
493 let key_bytes = unsafe { kvref.key().as_bytes() };
494 let key_str = std::str::from_utf8(key_bytes).expect("invariant violation: invalid utf-8 in storage");
495 dbg_map.entry(&key_str, kvref.value());
496 }
497 dbg_map.finish()
498 }
499}
500
501//------------------------------------------------------------------
502// Query API
503
504impl<RS: Send + Sync + Clone + BuildHasher> FrozenInterner<RS> {
505 pub fn get_str(&self, istr: IStr) -> &str {
506 self.frozen_storage.boxes[istr.arena_id() as usize].get_str(istr)
507 }
508}
509
510//----------------------------------------------------------------------------
511// Internal API - ArenaLocalIndex type definition
512
513#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
514struct ArenaLocalIndex {
515 raw: u32,
516}
517
518//------------------------------------------------------------------
519// Construction API
520
521impl ArenaLocalIndex {
522 #[inline]
523 fn new_chunk_index(index: u32) -> ArenaLocalIndex {
524 debug_assert!(index >> 31 == 0);
525 ArenaLocalIndex { raw: index }
526 }
527
528 #[inline]
529 fn new_huge_index(index: u32) -> ArenaLocalIndex {
530 debug_assert!(index >> 31 == 0);
531 ArenaLocalIndex {
532 raw: (1 << 31) | index,
533 }
534 }
535}
536
537//------------------------------------------------------------------
538// Query API
539
540impl ArenaLocalIndex {
541 #[inline]
542 const fn get_index_unchecked(&self) -> u32 {
543 (self.raw << 1) >> 1
544 }
545
546 #[inline]
547 const fn is_huge(&self) -> bool {
548 (self.raw >> 31) == 1
549 }
550
551 #[inline]
552 const fn chunk_index(&self) -> Option<u32> {
553 if self.is_huge() {
554 None
555 } else {
556 Some(self.get_index_unchecked())
557 }
558 }
559
560 #[inline]
561 const fn huge_index(&self) -> Option<u32> {
562 if self.is_huge() {
563 Some(self.get_index_unchecked())
564 } else {
565 None
566 }
567 }
568}
569
570//----------------------------------------------------------------------------
571// Internal API - CompositeIndex type definition
572
573// NOTE: We have a limit of 2^24 composite indices. How much string
574// storage does that correspond to?
575//
576// If we had all 3-byte u8 sequences, we'd need 2^24 composite indices.
577// So we can store at least 2^24 * 3 bytes = 48 MiB worth of strings.
578// However, given that not all possible 3-byte u8 sequences are valid
579// strings, plus the fact that people's code commonly reuses
580// identifiers longer than 3 bytes, I think it is safe to assume
581// that 24-bits for composite indices are enough.
582//
583// For context, at the time of writing, the total size of .cpp files
584// in the entire llvm-project is 81 MiB.
585
586#[derive(Copy, Clone, Debug)]
587struct CompositeIndex {
588 arena_local_index: ArenaLocalIndex,
589 chunk_start_offset: u16,
590 len: u16,
591}
592
593//------------------------------------------------------------------
594// Construction API
595
596impl CompositeIndex {
597 #[inline]
598 fn new_huge_index(index: usize) -> CompositeIndex {
599 debug_assert!(index <= u32::MAX as usize);
600 CompositeIndex {
601 arena_local_index: ArenaLocalIndex::new_huge_index(index as u32),
602 chunk_start_offset: u16::max_value(),
603 len: u16::max_value(),
604 } // Usage of u16::max_value() will trigger a panic on index confusion.
605 }
606
607 #[inline]
608 fn new_chunk_index(index: usize, chunk_start_offset: u16, len: u16) -> CompositeIndex {
609 debug_assert!(index <= u32::MAX as usize);
610 debug_assert!(
611 (chunk_start_offset as usize) < ConcurrentInternerMemberStorage::STORAGE_CHUNK_SIZE
612 );
613 debug_assert!(
614 chunk_start_offset as usize + (len as usize)
615 <= ConcurrentInternerMemberStorage::STORAGE_CHUNK_SIZE
616 );
617 CompositeIndex {
618 arena_local_index: ArenaLocalIndex::new_chunk_index(index as u32),
619 chunk_start_offset,
620 len,
621 }
622 }
623}
624
625//------------------------------------------------------------------
626// Query API
627
628impl CompositeIndex {
629 #[inline]
630 const fn huge_index(&self) -> Option<u32> {
631 self.arena_local_index.huge_index()
632 }
633
634 #[inline]
635 const fn chunk_info(&self) -> Option<(u32, u16, u16)> {
636 // Option::map is not const as of current Rust version. 😔
637 if let Some(chunk_index) = self.arena_local_index.chunk_index() {
638 Some((chunk_index, self.chunk_start_offset, self.len))
639 } else {
640 None
641 }
642 }
643}
644
645//----------------------------------------------------------------------------
646// Internal API - Small-size optimized &str.
647
648// TODO: Benchmark if SSOStringRef is actually faster than &str.
649//
650// This was originally implemented so that small strings would have
651// fast paths for comparison and hashing, but yeah, it leads to a bunch
652// of additional branching which may confuse a branch predictor.
653
654struct SSOStringRef {
655 inner: SSOStringRefInner,
656}
657
658//------------------------------------------------------------------
659// Helper type definition
660
661/// Implementation for an SSOStringRef.
662///
663/// The possible states are as follows:
664///
665/// 1. Empty string: Union is fully zeroed out.
666/// 2. Inline data: First byte is non-zero and string is either
667/// null-terminated or takes the full 2 words.
668/// The string does not contain nulls.
669/// 3. Non-inline data: First byte is zero, first word is length (upto
670/// shifts), second is ptr.
671#[repr(C)]
672union SSOStringRefInner {
673 sref: ByteSlice,
674 bytes: [u8; ByteSlice::SIZE_IN_BYTES],
675}
676
677//------------------------------------------------------------------
678// Trait implementations
679
680// SAFETY: This unsafe implementation is OK because any pointer
681// inside is only dereferenced when the storage is available.
682
683unsafe impl Send for SSOStringRef {}
684unsafe impl Sync for SSOStringRef {}
685
686impl PartialEq for SSOStringRef {
687 fn eq(&self, other: &SSOStringRef) -> bool {
688 // NOTE: Unlike C++, unions in Rust have no notion of "active field".
689 // So the punning between bytes and sref is ok.
690 // See https://doc.rust-lang.org/reference/items/unions.html
691 if unsafe { self.inner.sref.len != other.inner.sref.len } {
692 return false;
693 }
694 // Lengths are now bitwise equal.
695 unsafe {
696 if self.inner.sref.len == 0 {
697 // Both strings are empty.
698 return true;
699 } else if self.has_inline_data() {
700 // If the first string has inline data, then so does the
701 // second one, because otherwise their first bytes would've
702 // been different, so the len values would've been different.
703 //
704 // Even though we don't have pointers, instead of doing
705 // byte-wise comparison, we can treat the rest of the data as
706 // pointers and compare the two pointers.
707 std::debug_assert!(other.has_inline_data());
708 return self.inner.sref.ptr == other.inner.sref.ptr;
709 } else {
710 // Now we really have two pointers on our hands
711 return (self.inner.sref.ptr == other.inner.sref.ptr)
712 || (self.inner.sref.as_bytes() == other.inner.sref.as_bytes());
713 }
714 }
715 }
716}
717
718impl Eq for SSOStringRef {}
719
720impl Hash for SSOStringRef {
721 fn hash<H: Hasher>(&self, state: &mut H) {
722 (unsafe { self.as_bytes() }).hash(state);
723 }
724}
725
726//------------------------------------------------------------------
727// Construction API
728
729impl SSOStringRef {
730 /// Construct an SSOStringRef from a byte slice.
731 ///
732 /// NOTE: The bytes are assumed to be valid UTF-8 but this is not checked.
733 unsafe fn new_unchecked(s: &[u8]) -> SSOStringRef {
734 assert!(
735 s.len() <= ByteSlice::MAX_LEN,
736 "Trying to intern string bigger than 16 MiB on 32-bit?"
737 );
738 if s.len() == 0 {
739 SSOStringRef {
740 inner: SSOStringRefInner {
741 bytes: [0; ByteSlice::SIZE_IN_BYTES],
742 },
743 }
744 } else if s.len() <= ByteSlice::SIZE_IN_BYTES && s.iter().all(|&c| c != 0) {
745 let mut sso_stringref = SSOStringRef {
746 inner: SSOStringRefInner {
747 bytes: [0; ByteSlice::SIZE_IN_BYTES],
748 },
749 };
750 for (i, &c) in s.iter().enumerate() {
751 sso_stringref.inner.bytes[i] = c;
752 }
753 sso_stringref
754 } else {
755 SSOStringRef {
756 inner: SSOStringRefInner {
757 sref: ByteSlice::new(s),
758 },
759 }
760 }
761 }
762
763 pub fn new(s: &str) -> SSOStringRef {
764 // OK to use unsafe here since s is valid UTF-8.
765 unsafe { Self::new_unchecked(s.as_bytes()) }
766 }
767}
768
769//------------------------------------------------------------------
770// Query API
771
772impl SSOStringRef {
773 fn has_inline_data(&self) -> bool {
774 unsafe { self.inner.bytes[0] != 0 }
775 }
776
777 unsafe fn as_bytes<'a>(&'a self) -> &'a [u8] {
778 let s: &'a [u8];
779 if self.has_inline_data() {
780 let mut i = 1;
781 while i < ByteSlice::SIZE_IN_BYTES && self.inner.bytes[i] != 0 {
782 i += 1;
783 } // Post-condition: 1 <= i <= ByteSlice::SIZE_IN_BYTES
784 s = self
785 .inner
786 .bytes
787 .get(0..i)
788 .expect("Unexpected out-of-bounds byte index for SSOStringRef.");
789 } else if self.inner.sref.len() == 0 {
790 // If len() == 0, then the pointer may be null, which isn't allowed by as_bytes()
791 s = &[];
792 } else {
793 s = self.inner.sref.clone().as_bytes();
794 }
795 return s;
796 }
797}
798
799//----------------------------------------------------------------------------
800// Internal API - ByteSlice type definition
801
802/// Internal unsafe alternative to `&str` for doing bit nonsense.
803///
804/// Invariant: It is guaranteed that the first byte of this struct will be 0.
805#[derive(Copy, Clone)]
806#[repr(C)]
807struct ByteSlice {
808 len: usize,
809 ptr: *const u8,
810} // Reordering the fields here will require changing logic in SSOStringRef.
811
812//------------------------------------------------------------------
813// Constants
814
815impl ByteSlice {
816 const MAX_LEN: usize = usize::max_value() >> 8;
817 const SIZE_IN_BYTES: usize = std::mem::size_of::<Self>();
818}
819
820//------------------------------------------------------------------
821// Construction API
822
823impl ByteSlice {
824 #[cfg(target_endian = "little")]
825 fn new(s: &[u8]) -> ByteSlice {
826 std::debug_assert!(s.len() <= Self::MAX_LEN);
827 ByteSlice {
828 len: s.len() << 8,
829 ptr: s.as_ptr(),
830 }
831 }
832
833 /// The slice is assumed to point to a valid UTF-8 byte sequence,
834 /// but this is not checked.
835 #[cfg(target_endian = "big")]
836 fn new(s: &[u8]) -> ByteSlice {
837 std::debug_assert!(s.len() <= Self::MAX_LEN);
838 ByteSlice {
839 len: s.len(),
840 ptr: s.as_ptr(),
841 }
842 }
843}
844
845//------------------------------------------------------------------
846// Query API
847
848impl ByteSlice {
849 #[cfg(target_endian = "little")]
850 fn len(&self) -> usize {
851 self.len >> 8
852 }
853
854 #[cfg(target_endian = "big")]
855 fn len(&self) -> usize {
856 self.len
857 }
858
859 unsafe fn as_bytes<'a>(self) -> &'a [u8] {
860 // https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety
861 // says that passing a null pointer is not allowed, even for len = 0.
862 debug_assert!(self.ptr != std::ptr::null());
863 std::slice::from_raw_parts(self.ptr, self.len())
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 #[test]
870 fn test_istr_size() {
871 assert!(std::mem::size_of::<crate::IStr>() == 4);
872 }
873
874 use std::num::NonZeroU8;
875
876 use crate::{
877 ArenaLocalIndex, ConcurrentInterner, ConcurrentInternerMemberStorage, IStr, SSOStringRef,
878 };
879 use quickcheck::*;
880 quickcheck! {
881 fn test_sso_stringref_eq1(a: String, b: String) -> bool {
882 (a == b) == (SSOStringRef::new(&a) == SSOStringRef::new(&b))
883 }
884 fn test_sso_stringref_eq2(s: String) -> bool {
885 SSOStringRef::new(&s) == SSOStringRef::new(&s.clone())
886 }
887 }
888
889 quickcheck! {
890 fn test_arena_local_index_roundtrip(i: u32) -> bool {
891 let i = i & !(1 << 31);
892 assert_eq!(Some(i), ArenaLocalIndex::new_chunk_index(i).chunk_index());
893 assert_eq!(Some(i), ArenaLocalIndex::new_huge_index(i).huge_index());
894 return true;
895 }
896 }
897
898 quickcheck! {
899 fn test_insertion(vs: Vec<String>, n: u8, near_huge_idxs: Vec<u8>, huge_idxs: Vec<u8>) -> bool {
900 // Some bug in the quickcheck! macro doesn't allow mut in parameter position 🤔
901 let mut vs = vs;
902 if vs.len() == 0 {
903 return true;
904 }
905 // Overwrite the vec to have some near-huge strings
906 // (to exercise the chunk overflowing code paths)
907 // and some huge strings (to exercise the huge string code paths)
908 let chunk_size = ConcurrentInternerMemberStorage::STORAGE_CHUNK_SIZE;
909 // TODO: Clean up after https://github.com/BurntSushi/quickcheck/pull/311
910 // lands in quickcheck.
911 let mut near_huge_idxs = near_huge_idxs;
912 near_huge_idxs.truncate(3);
913 for i in near_huge_idxs.iter() {
914 let near_huge_idx = *i as usize;
915 if near_huge_idx < vs.len() {
916 vs[near_huge_idx] = (0 .. (chunk_size - 1 - near_huge_idx)).map(|_| 'k').collect();
917 }
918 }
919 let mut huge_idxs = huge_idxs;
920 huge_idxs.truncate(3);
921 for i in huge_idxs.iter() {
922 let huge_idx = *i as usize;
923 if huge_idx < vs.len() {
924 vs[huge_idx] = (0 .. (chunk_size + 1 + huge_idx)).map(|_| 'a').collect();
925 }
926 }
927
928 let n = NonZeroU8::new((n % 8) + 1).unwrap();
929 let interner = ConcurrentInterner::<ahash::RandomState>::new(n, Default::default());
930 let v: Vec<Vec<IStr>> = crossbeam::scope(|scope| {
931 let interner_ref = &interner;
932 let vsref = &vs;
933 let mut handles = vec![];
934 for _ in 0 .. n.get() {
935 handles.push(scope.spawn(move |_| {
936 let mut vout = Vec::with_capacity(vsref.len());
937 let mut member = interner_ref.get_member();
938 for s in vsref.iter() {
939 vout.push(member.intern(&s));
940 }
941 vout
942 }));
943 }
944 handles
945 .into_iter().map(|h| h.join().expect("Thread error!"))
946 .collect()
947 }).expect("crossbeam::scope had an error");
948
949 let istrs0 = &v[0];
950 assert!(istrs0.len() == vs.len());
951 for istrs in v.get(1 ..).expect("v.len() != 0").iter() {
952 assert!(istrs.len() == vs.len());
953 // Check that IStr values in different threads for the same strings
954 // are equal
955 for (i1, i2) in istrs0.iter().zip(istrs.iter()) {
956 assert!(*i1 == *i2);
957 }
958 }
959
960 // Check that get_string returns strings equivalent to the originals.
961 for istrs in v.iter() {
962 for (istr, s) in istrs.iter().zip(vs.iter()) {
963 assert!(interner.get_string(*istr) == *s);
964 }
965 }
966 return true;
967 }
968 }
969}