Lasso String Interner

repository·master·Indexed 41 days ago

https://github.com/kixiron/lasso

A high-performance string interner for Rust providing single-threaded (Rodeo) and multi-threaded (ThreadedRodeo) implementations. It minimizes memory footprint and offers contention-free resolution via specialized RodeoReader and RodeoResolver types. Version 0.7.3 supports custom-ranged keys, no-std environments, and various hashing algorithms like ahash and FXHash.

Tokens
13.7K
Snippets
40
Records
48
Agent score
50%

What's inside Lasso

  1. Choose the right interner for your workload

    master

    Lasso provides different interner types depending on whether your application is single-threaded or multi-threaded, and whether you need to continue interning strings or just resolve them.

    Decision Matrix

    InternerThread-safeIntern Stringstr to keykey to strContention FreeMemory Usage
    Rodeo❌✅✅✅N/AMedium
    ThreadedRodeo✅✅✅✅❌Most
    RodeoReader✅❌✅✅✅Medium
    RodeoResolver✅❌❌✅✅Least

    Usage Guidelines

    1. Single-threaded workloads: Use Rodeo.
    2. Multi-threaded workloads (active interning): Use ThreadedRodeo. Note that you must enable the multi-threaded Cargo feature to use this.
    3. Post-interning (Read-only access):
      • If you need to find keys for strings (str to key): Use RodeoReader. This is contention-free and can be shared across threads.
      • If you only need to resolve keys to strings (key to str): Use RodeoResolver. This provides the lowest possible memory usage and contention-free access.
  2. What is a RodeoResolver and when to use it

    master

    A RodeoResolver is a read-only view of a Rodeo or ThreadedRodeo that provides contention-free access to interned strings. It is optimized for minimal memory usage and is used specifically for key-to-string resolution.

    Once you have converted a Rodeo into a RodeoResolver (using .into_resolver()), you can no longer intern new strings, but you can efficiently resolve existing keys to their string representations. This is ideal for passing a read-only, thread-safe view of your interned data to other parts of your application.

    use lasso::Rodeo;
    
    let mut rodeo = Rodeo::default();
    let key = rodeo.get_or_intern("example string");
    
    // Convert the interner into a read-only resolver
    let resolver = rodeo.into_resolver();
    
    // Resolve the key
    assert_eq!("example string", resolver.resolve(&key));
  3. Configure Lasso Cargo features

    master

    Lasso uses Cargo features to manage dependencies and capabilities. By default, only Rodeo is exposed and hashbrown is used as the underlying hashmap.

    Available Features

    • multi-threaded: Enables ThreadedRodeo for multi-threaded tasks.
    • ahasher: Uses ahash's RandomState as the default hasher.
    • no-std: Enables no_std + alloc support for Rodeo and ThreadedRodeo. This automatically enables the ahasher feature.
    • serialize: Implements Serialize and Deserialize for all Spur types and all interners.
    • inline-more: Annotates external APIs with #[inline] to improve performance.
  4. Convert an Interner into a Reader or Resolver

    master

    To optimize for multi-threaded access or to reduce memory overhead, you can convert an Interner into specialized read-only types using the IntoReader and IntoResolver traits.

    IntoReader

    Use into_reader(self) -> Self::Reader to consume the Interner and convert it into a Reader. This is useful for transitioning from a mutable setup phase to a contention-free, multi-threaded reading phase.

    IntoResolver

    Use into_resolver(self) -> Self::Resolver to consume a Reader and convert it into a Resolver. This allows for contention-free access from multiple threads with the lowest possible memory consumption, as it focuses strictly on key-to-string resolution.

  5. Use RodeoReader for contention-free string resolution

    master

    A RodeoReader is a read-only view of a Rodeo or ThreadedRodeo that allows contention-free access to interned strings. It supports both key-to-string resolution and string-to-key lookups. You can create a reader by calling .into_reader() on an existing interner.

    Use RodeoReader when you need to share interned strings across multiple threads or parts of your application without the overhead of synchronization required by the primary interner.

    Key capabilities:

    • String to Key: Use .get(val) to find the key for a string slice.
    • Key to String: Use .resolve(key) or .try_resolve(key) to get the string slice from a key.
    • Membership: Use .contains(val) to check if a string is interned, or .contains_key(key) to check if a key is valid.
    • Iteration: Iterate over all interned strings using .iter() or .strings().
    use lasso::Rodeo;
    
    let mut rodeo = Rodeo::default();
    let key = rodeo.get_or_intern("Strings of things with wings and dings");
    
    // Convert the interner into a contention-free reader
    let reader = rodeo.into_reader();
    
    // String to Key lookup
    assert_eq!(Some(key), reader.get("Strings of things with wings and dings"));
    
    // Key to String resolution
    assert_eq!("Strings of things with wings and dings", reader.resolve(&key));
  6. Use ThreadedRodeo for multi-threaded string interning

    master

    A ThreadedRodeo is a concurrent string interner designed for high-performance, multi-threaded environments. It allows multiple threads to intern strings and resolve keys to string slices with minimal contention.

    Note: This struct is only available when the multi-threaded feature is enabled.

    By default, it uses Spur for keys and RandomState as the hasher. You can share a ThreadedRodeo across threads by wrapping it in an Arc.

    use lasso::{ThreadedRodeo, Spur};
    use std::{thread, sync::Arc};
    
    let lasso: Arc<ThreadedRodeo<Spur>> = Arc::new(ThreadedRodeo::new());
    let hello = lasso.get_or_intern("Hello, ");
    
    let l = Arc::clone(&lasso);
    let world = thread::spawn(move || {
        l.get_or_intern("World!")
    })
    .join()
    .unwrap();
    
    assert_eq!("Hello, ", lasso.resolve(&hello));
    assert_eq!("World!", lasso.resolve(&world));
  7. Convert Rodeo to thread-safe readers and resolvers

    master

    To allow multiple threads to access the interned strings without contention, you can consume the Rodeo and convert it into specialized read-only types.

    • into_reader(): Consumes the Rodeo and returns a RodeoReader. This allows contention-free access to the full interner (including the map for str -> key resolution).
    • into_resolver(): Consumes the Rodeo and returns a RodeoResolver. This is the most memory-efficient way to allow multi-threaded access if you only need to resolve key -> str.
    use lasso::Rodeo;
    
    let mut rodeo = Rodeo::new();
    rodeo.get_or_intern("Thread-safe string");
    
    // Convert to a reader for multi-threaded access
    let reader = rodeo.into_reader();
    // 'reader' can now be shared across threads
  8. Convert Rodeo to a Resolver or Reader

    master

    If you only need to resolve keys and do not need to modify the interner (intern new strings), you can convert a Rodeo into a more restricted view:

    • into_resolver(): Consumes the Rodeo and returns a type that only supports resolving keys to strings.
    • into_reader(): Consumes the Rodeo and returns a type that provides read-only access to the interned data.
  9. Initialize a Rodeo interner

    master

    The Rodeo struct is a single-threaded string interner. You can initialize it using several methods depending on your needs for capacity, memory limits, and hashing algorithms.

    • Rodeo::new(): Creates a default interner.
    • Rodeo::with_capacity(capacity): Pre-allocates capacity for strings using the Capacity type.
    • Rodeo::with_hasher(hasher): Creates an interner with a specific hasher.
    • Rodeo::with_capacity_and_hasher(capacity, hasher): Combines capacity and custom hashing.
    • Rodeo::with_capacity_and_memory_limits(capacity, memory_limits): Sets both capacity and memory usage constraints.
    • Rodeo::with_capacity_memory_limits_and_hasher(capacity, memory_limits, hasher): Provides full control over capacity, memory, and hashing.
    // Default
    let mut rodeo = Rodeo::new();
    
    // With capacity
    let mut rodeo = Rodeo::with_capacity(Capacity::for_strings(10));
    
    // With memory limits
    let mut rodeo = Rodeo::with_capacity_and_memory_limits(
        Capacity::for_bytes(NonZeroUsize::new(10).unwrap()),
        MemoryLimits::for_memory_usage(10),
    );
  10. Use Rodeo for single-threaded interning

    master

    The Rodeo interner is designed for single-threaded workloads. It allows you to intern strings to get unique keys and resolve those keys back to strings or find keys for existing strings.

    Key methods:

    • get_or_intern(s: &str): Returns the key for the string, interning it if it doesn't exist.
    • resolve(&key): Returns the string associated with the key.
    • get(s: &str): Returns Some(key) if the string is already interned, otherwise None.
    use lasso::Rodeo;
    
    let mut rodeo = Rodeo::default();
    let key = rodeo.get_or_intern("Hello, world!");
    
    // Easily retrieve the value of a key and find the key for values
    assert_eq!("Hello, world!", rodeo.resolve(&key));
    assert_eq!(Some(key), rodeo.get("Hello, world!"));
    
    // Interning the same string again will yield the same key
    let key2 = rodeo.get_or_intern("Hello, world!");
    
    assert_eq!(key, key2);
  11. Use ThreadedRodeo for multi-threaded interning

    master

    The ThreadedRodeo interner is thread-safe and allows multiple threads to intern strings simultaneously. To use this, you must enable the multi-threaded feature in your Cargo.toml.

    Because it is thread-safe, it is typically wrapped in an Arc to be shared across threads.

    use lasso::ThreadedRodeo;
    use std::{thread, sync::Arc};
    
    let rodeo = Arc::new(ThreadedRodeo::default());
    let key = rodeo.get_or_intern("Hello, world!");
    
    // Easily retrieve the value of a key and find the key for values
    assert_eq!("Hello, world!", rodeo.resolve(&key));
    assert_eq!(Some(key), rodeo.get("Hello, world!"));
    
    // Interning the same string again will yield the same key
    let key2 = rodeo.get_or_intern("Hello, world!");
    
    assert_eq!(key, key2);
    
    // ThreadedRodeo can be shared across threads
    let moved = Arc::clone(&rodeo);
    let hello = thread::spawn(move || {
        assert_eq!("Hello, world!", moved.resolve(&key));
        moved.get_or_intern("Hello from the thread!")
    })
    .join()
    .unwrap();
    
    assert_eq!("Hello, world!", rodeo.resolve(&key));
    assert_eq!("Hello from the thread!", rodeo.resolve(&hello));
  12. Convert an interner into a RodeoResolver

    master

    For the lowest possible memory usage and contention-free access, you can convert a Rodeo or ThreadedRodeo into a RodeoResolver using .into_resolver().

    RodeoResolver is optimized for read-only workloads where you only need to resolve keys to strings. It does not support string-to-key resolution (get).

    use lasso::Rodeo;
    
    // Rodeo and ThreadedRodeo are interchangeable here
    let mut rodeo = Rodeo::default();
    
    let key = rodeo.get_or_intern("Hello, world!");
    assert_eq!("Hello, world!", rodeo.resolve(&key));
    
    let resolver = rodeo.into_resolver();
    
    // Resolver keeps all the strings from the parent
    assert_eq!("Hello, world!", resolver.resolve(&key));
    
    // The Resolver can now be shared across threads, no matter what kind of Rodeo created it