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}