1  About fast.string

1.1 Where it sits

There’s no shortage of fast string packages for R. fast.string doesn’t try to replace any of them — it overlaps with each on one axis and fills a gap none of them cover on another:

Capability base R stringi / stringr stringdist RecordLinkage fast.string
Regex matching/substitution yes (TRE/PCRE) yes (ICU) — — yes (PCRE2)
Parallelised across cores no no partial (nthread) no yes (TBB)
Literal/fixed-string fast path no yes — — yes (prepared byte search)
Per-element match counting gregexpr() + length() stri_count() — — yes, parallelised (fcount())
Multi-pattern substitution in one scan no no — — yes (gsub_all())
Fast fixed-format date parse/format no no — — yes
Fast fixed-format timestamp parse/format no no — — yes (fas.POSIXct(), format_datetime())
Soundex / NYSIIS phonetic codes no no phonetic()/soundex soundex() yes, parallelised
Refined Soundex / Cologne phonetic codes no no no no yes (refined_soundex(), cologne())
Double Metaphone / Caverphone 2.0 no no no no yes (double_metaphone(), caverphone())
Jaro-Winkler similarity no no stringdist(method="jw") jarowinkler() yes, parallelised
Token-reorder-aware name comparison no no no no yes (jaro_winkler_tokens())
Levenshtein / OSA / unrestricted Damerau-Levenshtein / Hamming no no stringdist(method=...) no yes, parallelised
Best/top-N fuzzy table lookup without a score matrix agrep() (limited) no amatch() (best only) no yes (fuzzy_match(), fuzzy_top_n())
Jaccard / cosine q-gram similarity no no stringdist(method="jaccard"/"cosine") no yes, parallelised
Dice / Tversky q-gram similarity no no no no yes, parallelised
fuzzywuzzy-style ratios no no no no yes (port of Python’s fuzzywuzzy)

A few things worth calling out explicitly:

  • stringi/stringr are excellent, mature, and already parallel-friendly at the C level for many operations via ICU — but ICU’s regex engine and fast.string’s PCRE2 path land in similar performance territory for regex. Where fast.string pulls ahead is parallelism (stringi’s core loops are single-threaded; this package’s are TBB-parallel) and the data-linkage-specific functions stringi doesn’t have at all.
  • stringdist is the closest relative for the fuzzy-matching half of this package. fast.string now covers most of the same distance-metric ground (Levenshtein, OSA, unrestricted Damerau-Levenshtein, Hamming, Jaro-Winkler, Jaccard, cosine), plus a few stringdist doesn’t have at all (Dice, Tversky, token-reorder-aware comparison, fuzzywuzzy-style ratios). The benchmark chapter measures the overlapping kernels on the current checkout. What stringdist still has that this package doesn’t is LCS and raw q-gram-count distance — and it has nothing on the regex/substitution side, which is the other half of fast.string.
  • RecordLinkage is a full record-linkage framework (blocking, EM-based classification, training), not a string-function library. Its soundex()/jarowinkler() are useful comparison points (used directly in Chapter 6) but solve a much narrower problem than the package itself.

Chapter 6 puts numbers behind every row in that table at 3-million-row scale.

1.2 Architecture

The package is a thin R layer over compiled code; almost no decision logic lives in R beyond argument validation and dispatch:

flowchart LR
    A["R wrapper\n(fgrepl, ftrimws, jaro_winkler, ...)"] --> B{Argument\nvalidation}
    B -->|fixed = TRUE| C["Prepared literal search\n(byte scan / BMH)"]
    B -->|regex| E["PCRE2, UTF-8 aware\n(base R rules emulated\nwhen perl = FALSE)"]
    B -->|trim/substr/nchar/chartr/\ndate/phonetic/jaro-winkler| F["Custom C++\n(via Rcpp)"]
    E --> G["RcppParallel\n(Intel TBB parallelFor)"]
    F --> G
    C --> G
    G --> H[Result vector]

Three design decisions explain most of the package’s behaviour:

Separate regex and fixed-literal paths, chosen automatically. PCRE2 supports the full modern regex feature set (lookaround, atomic groups, possessive quantifiers, named backreferences) that R’s default TRE engine doesn’t. The common fixed/literal case (fixed = TRUE) prepares the pattern once, then chooses memchr for one byte, memchr plus memcmp for two or three bytes, or Boyer–Moore–Horspool for longer patterns. Case-insensitive fixed matching uses a 256-entry byte-folding table instead of allocating lowercased copies of each input. Because this path interprets the pattern only as bytes, it has no regex backtracking risk. Every regular expression runs on PCRE2. With the default perl = FALSE the pattern is rewritten and compiled to follow base R’s default (TRE) rules – . matches a newline, $ only matches at the very end, backslashes are literal inside brackets, \</\> are word boundaries – while patterns that only PCRE understands (lookarounds, atomic groups, …) get perl = TRUE semantics. Text is matched by UTF-8 character unless useBytes = TRUE. See Chapter 3 for the details.

Parallelism via Intel TBB, not parallel/future. Forking R processes (parallel::mclapply) or spinning up a cluster (future) carries fixed overhead per call — serialising data to workers, process startup, gathering results back — that swamps the actual work for anything shorter than seconds. RcppParallel’s parallelFor instead splits one C++-level loop across a TBB thread pool within a single R call, with no serialisation step and microsecond-scale dispatch overhead. Every parallel function in this package applies a size threshold before bothering to parallelise at all (typically n >= 1000 for vectors, n*m >= 10000 for matrix operations) — below that, the thread-pool dispatch overhead would exceed the time saved, so the package just runs the loop on the calling thread.

Thread-local scratch buffers, not per-call allocation. The performance-sensitive C++ (the bit-parallel Jaro similarity, the token alignment in jaro_winkler_tokens()) reuses thread_local buffers across rows within a worker thread instead of allocating fresh memory for every string pair. The buffers grow to a high-water mark and then stop reallocating, so the steady-state cost of comparing the one-millionth pair is the same as the thousandth.

1.3 Who this is for

If you’re doing record linkage, deduplication, ETL, or any other pipeline that processes name/address/free-text columns with hundreds of thousands to tens of millions of rows, and you’ve profiled your pipeline and found string operations are a meaningful fraction of the runtime, this package is squarely aimed at you.

If your data is a few thousand rows, or your bottleneck is somewhere else entirely (I/O, a join, a model fit), the speedups here won’t be the thing that matters, and the simpler base R/stringi call is the right choice — fewer dependencies, identical behaviour, and the performance difference won’t be visible at that scale. Chapter 6 shows this explicitly: at small n, the parallel-dispatch overhead can make fast.string functions slower than their base R/stringi equivalents.