aho-corasick

repository·master·Indexed 23 days ago

https://github.com/burntsushi/aho-corasick

A high-performance Rust library for simultaneous multiple substring matching using the Aho-Corasick algorithm. It features SIMD acceleration via the Teddy algorithm, support for NFA and DFA implementations, and configurable match semantics including LeftmostFirst and LeftmostLongest. The library supports ASCII case-insensitive matching and stream-based replacement.

Tokens
11.6K
Snippets
18
Records
53
Agent score
78%

What's inside aho-corasick

  1. Understand the `rust-aho-corasick` benchmark runner

    master

    The rust-aho-corasick runner is a program designed to benchmark the aho-corasick crate. It is specifically optimized for searching literal strings rather than complex regular expressions.

    Key characteristics:

    • Literal Search Only: The runner treats all regex patterns as literals. It should only be used for benchmarks where the patterns are literal strings.
    • Supported Benchmark Models: The runner only supports the following models:
      • compile
      • count
      • count-spans
      • grep
  2. Understand the purpose of the rust-old-aho-corasick benchmark runner

    master

    The rust-old-aho-corasick directory contains a Rust runner program designed specifically for benchmarking the aho-corasick crate. It is used to compare different implementations of the Aho-Corasick algorithm, specifically the nfa and dfa engines.

    Note that this runner treats all regex patterns as literal strings because the aho-corasick crate only supports searching for literal strings. Consequently, this runner is only suitable for benchmarking literal-based patterns.

  3. What is Aho-Corasick and how does it work?

    master

    Aho-Corasick is a multiple substring matching algorithm used to find occurrences of many patterns within a single haystack. Unlike naive searching, which checks every pattern at every position, Aho-Corasick visits each byte in the haystack exactly once, making its performance largely independent of the number of patterns (though larger automata may impact CPU cache efficiency).

    At its core, the algorithm uses a trie augmented with failure transitions.

    • Trie: A tree structure representing the patterns.
    • Failure Transitions: When a byte in the haystack does not match a transition in the current state, the algorithm follows a failure transition to a state representing the longest proper suffix of the current path that is also a prefix of another pattern. This allows the search to continue without restarting from the beginning of the haystack.

    This implementation supports several variants, including:

    • NFA (Noncontiguous/Contiguous): Different memory layouts for state transitions.
    • DFA: A dense transition table using a single allocation.
    • Match Semantics: Standard, overlapping, leftmost-first, and leftmost-longest.
    • Accelerations: ASCII case insensitivity, SIMD-vectorized search (Teddy algorithm), and fixed-byte/rare-byte acceleration via memchr.
  4. What is the Teddy algorithm?

    master

    Teddy is a SIMD-accelerated multiple substring matching algorithm. It is designed for high-performance scanning of a haystack to find multiple patterns simultaneously. It is particularly effective for short substrings, making it a suitable component for accelerating regex engines by searching for small sets of required literals extracted from a regex.

    Key characteristics:

    • SIMD Accelerated: Uses SIMD instructions (like PSHUFB from SSSE3) to process data in parallel.
    • Chunk-based Scanning: Scans the haystack in 16-byte chunks (for SSE) or 32-byte chunks (for AVX).
    • Fingerprint Matching: Uses bitwise operations and bitsets to quickly identify potential matches via precomputed fingerprints, followed by a verification step.
  5. Understand how prefilters accelerate searching

    master

    To improve performance, the crate uses prefilters to quickly scan for specific bytes before executing the full Aho-Corasick automaton. This is particularly effective when patterns share common or rare bytes.

    Prefilter Strategies:

    • Leading Byte Search: If patterns have few distinct starting bytes (e.g., S, M, or W), the crate scans for these bytes first.
    • Rare Byte Search: If there are more than three distinct starting bytes, the crate looks for up to three distinct bytes that appear anywhere in the patterns, prioritizing bytes that are heuristically determined to be rare (e.g., searching for z in a set of patterns containing it).
    • Teddy Algorithm: A packed multiple substring algorithm that confirms its own matches. If Teddy successfully finds matches, the Aho-Corasick automaton may not need to run at all.
  6. Choose between NFA and DFA automaton types

    master

    The crate allows you to choose between Non-deterministic Finite Automata (NFA) and Deterministic Finite Automata (DFA) to balance memory usage and search speed.

    • NFA (Non-deterministic Finite Automata): Uses significantly less memory and is faster to build. A contiguous NFA is the default choice for most circumstances. It provides search speeds close to a DFA while maintaining a much smaller memory footprint.
    • DFA (Deterministic Finite Automata): Offers faster search performance because it uses a single state transition table and avoids following failure transitions during search. However, it is more costly to build and uses much more memory.

    By default, the crate automatically selects a contiguous NFA, but it will switch to a DFA if the number of patterns is small enough. You can override this automatic selection using AhoCorasickBuilder::start_kind.

  7. Configure match semantics with MatchKind

    master

    By default, Aho-Corasick may report matches as soon as they are seen, which can lead to unexpected results when patterns overlap (e.g., matching Sam instead of Samwise in the text Samwise).

    To achieve regex-like behavior, use AhoCorasick::builder() and set the .match_kind() method:

    • MatchKind::LeftmostFirst: Matches the leftmost occurrence, and among those, the one that starts earliest (similar to Perl-like regex engines).
    • MatchKind::LeftmostLongest: Matches the leftmost occurrence, and among those, the longest one (similar to POSIX regex behavior).

    Example of LeftmostFirst matching:

    use aho_corasick::{AhoCorasick, MatchKind};
    
    let patterns = &["Samwise", "Sam"];
    let haystack = "Samwise";
    
    let ac = AhoCorasick::builder()
        .match_kind(MatchKind::LeftmostFirst)
        .build(patterns)
        .unwrap();
    let mat = ac.find(haystack).expect("should have a match");
    assert_eq!("Samwise", &haystack[mat.start()..mat.end()]);
  8. Understand Aho-Corasick match semantics

    master

    Match semantics define how the automaton disambiguates multiple possible matches. You can select these via the MatchKind type.

    • standard: Emits matches as soon as they are detected. This is the textbook non-overlapping formulation.
    • leftmost-first:
      1. Finds the match starting at the leftmost position.
      2. If multiple matches start at the same position, it reports the one corresponding to the pattern provided first by the caller (allowing for match priority).
    • leftmost-longest:
      1. Finds the match starting at the leftmost position.
      2. If multiple matches start at the same position, it reports the longest match (useful for dictionary word matching).

    Note on Overlapping Matches: If you require overlapping matches (reporting all possible matches even when they intersect), you must use standard match semantics. Attempting an overlapping search with leftmost-first or leftmost-longest will result in an error or panic because those modes use a subset of failure transitions that prevent overlapping detection.

  9. Compare Teddy and Fat Teddy variants

    master

    The aho-corasick implementation provides two main variants of the Teddy algorithm depending on the available hardware and search requirements:

    Teddy (Standard)

    • Buckets: Uses 8 buckets to group patterns.
    • SIMD Usage: Can scan 32-byte chunks when using AVX.
    • Masks: Uses 8-bit representations for bucket assignments.

    Fat Teddy

    • Buckets: Extends to 16 buckets.
    • SIMD Usage: Limited to scanning 16 bytes at a time, even with AVX, due to the way masks are structured.
    • Masks: Uses 16-bit representations for bucket assignments.
    • Trade-off: While it scans fewer bytes per iteration, it reduces the work in the verification routine by spreading patterns across more buckets, resulting in fewer false positives. This allows the algorithm to handle more literals before becoming overwhelmed.
  10. How the Teddy algorithm works

    master

    Teddy achieves high throughput by using bitwise operations and SIMD instructions to perform 'packed' substring matching. The high-level process is:

    1. Fingerprinting: For each pattern, a fingerprint is computed (this implementation uses the first $N$ bytes of each substring).
    2. Bitset Mapping: A map is created where each fingerprint (or its nybble components) maps to a bitfield representing the patterns that contain it.
    3. SIMD Lookup: The haystack is scanned in 16 or 32-byte chunks. The algorithm uses the PSHUFB instruction to perform a parallel lookup. It splits each byte of the haystack into two nybbles (lower 4 bits and upper 4 bits) and uses them to index into two precomputed bitset masks (A0 for lower nybbles, A1 for upper nybbles).
    4. Result Combination: The results from the lower and upper nybble lookups are combined using an AND operation to produce a bitset C. If any bit in C is set, it indicates a potential match at that position.
    5. Verification: The position of the least significant bit in C identifies the pattern and the offset within the chunk. A verification step is then performed to confirm the actual match.
  11. Perform stream searching for non-contiguous haystacks

    master

    Aho-Corasick is an automaton, which allows for partial searches on parts of a haystack that can be resumed on subsequent pieces. This is useful when the haystack is not stored contiguously in memory or when you want to avoid reading the entire haystack into memory at once.

    Limitations:

    • Currently, only standard semantics are supported for stream searching.
    • The implementation requires buffering at least enough of the haystack in memory to accommodate the longest possible match.
  12. Understand the Teddy SIMD algorithm implementation

    master

    Teddy is a SIMD-accelerated multiple substring matching algorithm. It optimizes performance by scanning large chunks (16 or 32 bytes) at a time using fingerprints.

    Key implementation details include:

    • Fingerprint Length (N): The algorithm uses a prefix of length $N \in {1, 2, 3}$ bytes. It automatically selects the largest possible $N$ to minimize the number of verification steps. Larger fingerprints reduce false positives but increase the work required per step.
    • Verification: When a fingerprint match is detected via SIMD bitsets, the algorithm performs a verification step. It extracts 64-bit integers from the SIMD vectors, identifies the least significant bit to derive the byte offset and bucket, and then performs an exhaustive search for literals within that bucket.
    • SIMD Scaling: The algorithm scales from SSE (128-bit) to AVX (256-bit). While AVX allows for 32-byte chunks, certain alignment operations (like PALIGNR in SSE) require specific shuffling workarounds in AVX because VPALIGNR only operates within 128-bit lanes.