difflib-fast 0.4.0__cp39-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ """difflib-fast — fast, byte-for-byte exact difflib Ratcliff-Obershelp similarity + clustering.
2
+
3
+ A drop-in for ``difflib.SequenceMatcher(None, a, b, autojunk=False).ratio()``, computed with a suffix
4
+ automaton (Rust), plus exact single-linkage clustering of a corpus.
5
+
6
+ ``ratio`` is overloaded: two strings → one float; a list of ``(a, b)`` pairs → a list of floats,
7
+ computed across all cores inside Rust (rayon, GIL released) — the contention-free way to score a batch.
8
+
9
+ ``Rationer`` is the stateful handle: on a macOS wheel built with the ``gpu`` feature it runs
10
+ ``cluster_canonicals`` on the Metal GPU (else CPU, identical output).
11
+ """
12
+
13
+ from typing import overload
14
+
15
+ from ._difflib_fast import (
16
+ CosineJoiner,
17
+ Rationer,
18
+ cluster_canonicals,
19
+ cluster_canonicals_lsh,
20
+ cosine_join,
21
+ ratio as _ratio,
22
+ ratio_many as _ratio_many,
23
+ )
24
+
25
+ __all__ = [
26
+ "ratio",
27
+ "cluster_canonicals",
28
+ "cluster_canonicals_lsh",
29
+ "Rationer",
30
+ "cosine_join",
31
+ "CosineJoiner",
32
+ ]
33
+
34
+
35
+ @overload
36
+ def ratio(a: str, b: str, /) -> float: ...
37
+ @overload
38
+ def ratio(pairs: list[tuple[str, str]], /, *, threads: int = 0) -> list[float]: ...
39
+ def ratio(a, b=None, /, *, threads=0):
40
+ """Exact ``difflib.SequenceMatcher(None, a, b, autojunk=False).ratio()`` — byte-for-byte.
41
+
42
+ - ``ratio(a, b)`` → one float for the pair.
43
+ - ``ratio(pairs)`` → one float per ``(a, b)`` pair, computed in parallel across all cores inside
44
+ Rust (the GIL is released), so a batch saturates every core with no ``ThreadPoolExecutor`` and no
45
+ per-call overhead. ``ratio(pairs)[i] == ratio(*pairs[i])``, in order. Pass ``threads=N`` to cap
46
+ the pool to N workers for this call (``threads=0``, the default, uses every core — itself tunable
47
+ process-wide via the ``RAYON_NUM_THREADS`` environment variable).
48
+ """
49
+ if b is None:
50
+ return _ratio_many(a, threads)
51
+ return _ratio(a, b)
@@ -0,0 +1,125 @@
1
+ """Type stubs for difflib-fast — exact difflib Ratcliff-Obershelp similarity + clustering."""
2
+
3
+ from typing import overload
4
+
5
+ @overload
6
+ def ratio(a: str, b: str, /) -> float:
7
+ """Exact ``difflib.SequenceMatcher(None, a, b, autojunk=False).ratio()`` — byte-for-byte.
8
+
9
+ The Ratcliff-Obershelp similarity ``2*M / (len(a) + len(b))``, identical to Python's ``difflib``
10
+ (including its argument-order asymmetry), computed via a suffix automaton so it stays linear on
11
+ long, repetitive inputs where ``difflib`` degrades.
12
+ """
13
+
14
+ @overload
15
+ def ratio(pairs: list[tuple[str, str]], /, *, threads: int = 0) -> list[float]:
16
+ """Exact :func:`ratio` for many ``(a, b)`` pairs at once, computed across all cores inside Rust.
17
+
18
+ ``ratio(pairs)[i] == ratio(*pairs[i])``, bit-for-bit. The parallel fan-out happens in Rust with the
19
+ GIL released — the contention-free way to score a batch from Python: no ``ThreadPoolExecutor``, no
20
+ per-call overhead. Returns one float per input pair, in order. ``threads=N`` caps the pool to N
21
+ workers for this call; ``threads=0`` (default) uses every core (tunable via ``RAYON_NUM_THREADS``).
22
+ """
23
+
24
+ def cluster_canonicals(
25
+ canonicals: list[str], threshold: float, threads: int = 0
26
+ ) -> list[tuple[list[int], float]]:
27
+ """Exact single-linkage clustering of ``canonicals`` by RO similarity.
28
+
29
+ Two strings join a cluster when their exact ratio is ``>= threshold``. Returns one
30
+ ``(member_indices, min_pairwise_ratio)`` per cluster of >= 2 members; ``member_indices`` index
31
+ into ``canonicals`` (sorted), ``min_pairwise_ratio`` is the cluster's exact minimum pairwise ratio.
32
+ The all-pairs join runs across all cores (GIL released); ``threads=N`` caps the pool to N for this
33
+ call (``threads=0``, default, uses every core — tunable via ``RAYON_NUM_THREADS``).
34
+ """
35
+
36
+ def cluster_canonicals_lsh(
37
+ canonicals: list[str], threshold: float, num_perm: int, band_rows: int, threads: int = 0
38
+ ) -> list[tuple[list[int], float]]:
39
+ """Scalable MinHash-LSH variant of :func:`cluster_canonicals` for very large corpora.
40
+
41
+ Generates candidate pairs via MinHash-LSH (``num_perm`` permutations, ``band_rows`` rows per band),
42
+ then verifies each candidate with the exact ratio. Clusters match the exact path modulo LSH recall
43
+ (tuned via ``band_rows``); use :func:`cluster_canonicals` when exact recall is required.
44
+ ``threads=N`` caps the pool to N workers for this call (``threads=0``, default, uses every core).
45
+ """
46
+
47
+ def cosine_join(
48
+ docs: list[list[str]], threshold: float, concurrency: str = "cpu", threads: int = 0
49
+ ) -> list[tuple[int, int, float]]:
50
+ """Exact all-pairs weighted-cosine similarity join over token documents.
51
+
52
+ Each ``doc`` is a list of string tokens (e.g. a function's canonicalised lines); they're turned
53
+ into TF-IDF sparse vectors in Rust (dim = distinct token, weight = token-count × ``ln(n/df)``) and
54
+ every pair with cosine ``>= threshold`` is returned as ``(j, i, cos)`` with ``j < i``. Like
55
+ :func:`ratio`'s batch form, the work fans out across **all cores inside Rust with the GIL
56
+ released** — one call, full multicore, no ``ThreadPoolExecutor``. ``threads=N`` caps the pool to N
57
+ for this call; ``threads=0`` (default) uses every core (tunable via ``RAYON_NUM_THREADS``).
58
+
59
+ ``concurrency`` ∈ ``"cpu" | "gpu" | "gpu+cpu"``: ``"cpu"`` is exact f64; ``"gpu+cpu"`` is the exact
60
+ f64 GPU-accelerated hybrid (byte-identical to ``"cpu"``); ``"gpu"`` runs the dot on the Metal GPU
61
+ in f32 (fastest; differs from exact only on pairs within ~1e-6 of the threshold). Off a macOS
62
+ ``gpu`` wheel, or with no Metal device, the GPU modes quietly fall back to CPU. For repeated joins
63
+ on one corpus use :class:`CosineJoiner` (builds the corpus / uploads to the GPU once).
64
+ """
65
+
66
+ class CosineJoiner:
67
+ """Stateful similarity-join handle: builds the TF-IDF corpus and (on a macOS ``gpu`` wheel)
68
+ acquires the Metal device + uploads the corpus once, then answers repeated joins reusing them.
69
+
70
+ Use it to sweep thresholds — the free :func:`cosine_join` rebuilds everything per call.
71
+ """
72
+
73
+ def __init__(self, docs: list[list[str]]) -> None:
74
+ """Build a joiner over token documents (each a list of string tokens)."""
75
+
76
+ def __len__(self) -> int:
77
+ """Number of documents in the corpus."""
78
+
79
+ @property
80
+ def has_gpu(self) -> bool:
81
+ """Whether a Metal GPU backend was acquired (always ``False`` off a macOS ``gpu`` wheel)."""
82
+
83
+ def join(
84
+ self, threshold: float, concurrency: str = "cpu", threads: int = 0
85
+ ) -> list[tuple[int, int, float]]:
86
+ """Join at ``threshold`` under ``concurrency`` (``"cpu" | "gpu" | "gpu+cpu"``), reusing the
87
+ handle's resources. Returns ``(j, i, cos)`` pairs with ``j < i``; fans out across all cores
88
+ with the GIL released (``threads=0`` = every core)."""
89
+
90
+ class Rationer:
91
+ """Stateful similarity/clustering handle that owns the backend resources once and reuses them.
92
+
93
+ On a macOS wheel built with the ``gpu`` feature, ``cluster_canonicals`` runs on the Metal GPU
94
+ when the group is large enough to amortize dispatch (~1.1-1.4x vs CPU on Apple Silicon); on every
95
+ other wheel, or with no Metal device, it transparently runs on CPU with byte-for-byte identical
96
+ output. ``ratio`` / ``ratio_many`` always run on CPU (the GPU offload measured slower there).
97
+ """
98
+
99
+ def __init__(self, concurrency: str = "gpu+cpu", threads: int = 0, delta: float = 0.0) -> None:
100
+ """Build a handle.
101
+
102
+ - ``concurrency``: ``"cpu"``, ``"gpu"``, or ``"gpu+cpu"`` (default). On a non-Metal build/host
103
+ ``"gpu"``/``"gpu+cpu"`` quietly fall back to CPU.
104
+ - ``threads``: rayon worker count for CPU-side work; ``0`` (default) uses every core.
105
+ - ``delta``: approximate-RO knob; ``0.0`` (default) = exact, bit-identical to ``difflib``.
106
+ """
107
+
108
+ @property
109
+ def concurrency(self) -> str:
110
+ """Active backend after construction-time fallback: ``"cpu"``, ``"gpu"``, or ``"gpu+cpu"``."""
111
+
112
+ @property
113
+ def delta(self) -> float:
114
+ """Active approximate-RO ``delta`` (``0.0`` = exact)."""
115
+
116
+ def ratio(self, a: str, b: str) -> float:
117
+ """Single-pair exact ratio (always CPU)."""
118
+
119
+ def ratio_many(self, pairs: list[tuple[str, str]]) -> list[float]:
120
+ """Batched exact ratio over ``(a, b)`` pairs, across all cores (GIL released). Always CPU."""
121
+
122
+ def cluster_canonicals(self, canonicals: list[str], threshold: float) -> list[tuple[list[int], float]]:
123
+ """Exact single-linkage clustering at ``threshold``; GPU-accelerated where it wins (see class doc)."""
124
+
125
+ __all__: list[str]
Binary file
difflib_fast/py.typed ADDED
File without changes
@@ -0,0 +1,423 @@
1
+ Metadata-Version: 2.4
2
+ Name: difflib-fast
3
+ Version: 0.4.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Topic :: Text Processing :: General
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Typing :: Typed
9
+ Summary: Fast, byte-for-byte exact difflib Ratcliff-Obershelp similarity + clustering.
10
+ Keywords: difflib,similarity,ratcliff-obershelp,fuzzy,diff
11
+ Author: prostomarkeloff
12
+ License: MIT
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: Repository, https://github.com/prostomarkeloff/difflib-fast
16
+
17
+ <div align="center">
18
+
19
+ # difflib-fast
20
+
21
+ **The exact `difflib` similarity ratio — up to `8,500×` faster.**
22
+
23
+ [![Rust 2021](https://img.shields.io/badge/rust-2021-orange.svg)](https://www.rust-lang.org/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
25
+ [![exact difflib](https://img.shields.io/badge/difflib-byte--for--byte-blue.svg)](https://docs.python.org/3/library/difflib.html)
26
+ [![vs difflib: up to 8,500×](https://img.shields.io/badge/vs%20difflib-up%20to%208%2C500%C3%97-brightgreen.svg)](#python-package)
27
+
28
+ The **same number** Python's `difflib` gives you — byte-for-byte, no `autojunk` approximation. A corpus
29
+ stdlib `difflib` would chew on for **~30 minutes** clusters in **~0.2 seconds**.
30
+
31
+ </div>
32
+
33
+ ```python
34
+ import difflib_fast
35
+
36
+ difflib_fast.ratio("the quick brown fox", "the quick brown dog") # 0.8947368421052632 (== difflib)
37
+ difflib_fast.ratio(pairs) # list[float] — computed across every core inside Rust, GIL released
38
+ ```
39
+
40
+ <div align="center">
41
+
42
+ | from Python · real corpus · 12 cores | throughput | vs stdlib `difflib` |
43
+ |---|---|---|
44
+ | `ratio(a, b)` — one call | 2.4k pairs/s | **104×** |
45
+ | `ratio(pairs)` — batch, all cores | 15k pairs/s | **628×** |
46
+ | `cluster_canonicals(corpus)` — the real workload | 199k pairs/s | **8,541×** |
47
+
48
+ *23 pairs/s → 199,000 pairs/s on the same task. Same answer. ([how ↓](#how-it-works))*
49
+
50
+ </div>
51
+
52
+ And a pure-Rust crate, with **zero** Python dependency by default:
53
+
54
+ ```rust
55
+ use difflib_fast::ratio;
56
+ // bit-for-bit identical to difflib.SequenceMatcher(None, a, b, autojunk=False).ratio()
57
+ assert_eq!(ratio("the quick brown fox", "the quick brown dog"), 0.8947368421052632);
58
+ ```
59
+
60
+ ```toml
61
+ difflib-fast = "0.3"
62
+ ```
63
+
64
+ ---
65
+
66
+ ## The idea
67
+
68
+ `ratio = 2·M / (len(a) + len(b))`, where `M` is the total size of the Ratcliff–Obershelp matching
69
+ blocks. The metric is exact and well-defined — including `difflib`'s tie-break **and its
70
+ argument-order asymmetry**, both of which this crate reproduces bit-for-bit.
71
+
72
+ The catch is how you compute `M`. `difflib` does it by re-scanning every occurrence of each character;
73
+ on long, small-alphabet text — canonicalized source code, log lines, DNA — a handful of popular
74
+ characters turn that into a quadratic crawl. `difflib-fast` computes the longest common substring with
75
+ a **suffix automaton** in `O(|a|+|b|)` regardless of how often a character repeats. Same answer. No
76
+ crawl.
77
+
78
+ > Two independent implementations of `M` live in this crate — the suffix automaton, and a from-scratch
79
+ > port of `difflib`'s own recursion. The test suite asserts they are **bit-identical**. That equality
80
+ > *is* the correctness guarantee.
81
+
82
+ ---
83
+
84
+ ## What it does
85
+
86
+ Two things, both exact:
87
+
88
+ ```rust
89
+ use difflib_fast::{ratio, cluster_canonicals};
90
+
91
+ // 1. pairwise similarity — a drop-in for difflib's ratio
92
+ let r = ratio("def add(a, b): return a + b", "def add(x, y): return x + y");
93
+
94
+ // 2. cluster a whole corpus by similarity (single-linkage, exact min pairwise ratio per cluster)
95
+ let corpus = vec![
96
+ "def add(a, b): return a + b".to_string(),
97
+ "def add(x, y): return x + y".to_string(),
98
+ "totally unrelated".to_string(),
99
+ ];
100
+ let clusters = cluster_canonicals(&corpus, 0.5); // → [([0, 1], 0.84…)]
101
+ ```
102
+
103
+ The clustering path is the one built for scale: each string's automaton is **prebuilt once** and
104
+ reused across the whole `n²` join, dissimilar pairs **early-exit** the moment the threshold is decided,
105
+ and the work is spread across cores with `rayon`. A cluster's reported `min_sim` is its exact minimum
106
+ pairwise ratio.
107
+
108
+ ---
109
+
110
+ ## API
111
+
112
+ Two tiers. The **free functions** are the stateless CPU drop-in:
113
+
114
+ | function | what |
115
+ |---|---|
116
+ | `ratio(a, b) -> f64` | exact `difflib` ratio (dispatches automaton ⇄ `b2j` per input) |
117
+ | `ratio_many(&[(String, String)]) -> Vec<f64>` | exact `ratio` for a batch of pairs, in parallel (rayon) |
118
+ | `gestalt::gestalt_ratio(a, b) -> f64` | the suffix-automaton path directly |
119
+ | `cluster_canonicals(&[String], threshold) -> Vec<(Vec<usize>, f64)>` | exact single-linkage clusters + min pairwise ratio |
120
+ | `cluster_canonicals_lsh(&[String], threshold, num_perm, band_rows)` | scalable MinHash-LSH variant (candidate-gen + exact verify) for very large corpora |
121
+
122
+ And `Rationer` is the **stateful handle** that owns long-lived resources (rayon pool, and on
123
+ macOS the Metal device) once and reuses them across calls — same exact output, with an optional GPU
124
+ path:
125
+
126
+ ```rust
127
+ use difflib_fast::Rationer;
128
+
129
+ let r = Rationer::new(); // builder().build(); default GpuPlusCpu
130
+ let clusters = r.cluster_canonicals(&corpus, 0.6); // GPU-accelerated on macOS (see below)
131
+ let ratios = r.ratio_many(&pairs); // CPU
132
+ ```
133
+
134
+ `Rationer::builder().concurrency(Concurrency::Cpu | Gpu | GpuPlusCpu).threads(n).delta(d).build()`
135
+ configures it; `PreparedRationer` (`r.prepare(&strings)`) amortizes SAM-build over many index-pair
136
+ queries.
137
+
138
+ ### GPU (macOS + Metal)
139
+
140
+ Behind the `gpu` cargo feature on Apple Silicon, `Rationer::cluster_canonicals` offloads the
141
+ suffix-automaton `matching_stats` walk to a Metal compute kernel — **byte-for-byte identical output**,
142
+ **~1.1–1.4×** end-to-end vs the (already fast) CPU path on a single large group:
143
+
144
+ | corpus | `cluster_canonicals` GPU vs CPU |
145
+ |---|---|
146
+ | mypy | **1.38×** |
147
+ | ha | 1.25× |
148
+ | sympy | 1.17× |
149
+ | django | 1.14× |
150
+ | transformers | 1.08× |
151
+
152
+ It's a modest, honest win: the GPU only does `matching_stats` (~⅓ of the per-pair cost); the
153
+ `longest_in` recursion, filtering and assembly stay on CPU, so Amdahl caps it. `ratio_many` and
154
+ `cluster_canonicals_multi` measured *slower* on the GPU at every size tested, so they stay on CPU by
155
+ default (the GPU paths remain opt-in via `DFGPU_RATIO_MANY_THRESHOLD` / `DFGPU_MULTI_THRESHOLD`). With
156
+ the feature off, on non-macOS, or with no Metal device, every call quietly runs on CPU.
157
+
158
+ ```toml
159
+ difflib-fast = { version = "0.3", features = ["gpu"] } # macOS only
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Correctness
165
+
166
+ This crate's entire reason to exist is being *exactly* `difflib` — so correctness is enforced, not
167
+ hoped for:
168
+
169
+ - **two implementations, one answer** — the suffix-automaton path and the `b2j` reference port are
170
+ asserted bit-identical on thousands of fuzzed pairs (`fast_matches_reference`);
171
+ - **18k-assertion threshold gate** — every early-exit decision matches the full ratio
172
+ (`qualifies_matches_ratio_threshold`);
173
+ - **`difflib` reference values**, including non-ASCII.
174
+
175
+ ```bash
176
+ cargo test
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Benchmarks
182
+
183
+ Exact byte-for-byte RO on real canonicalized Python (top-level function bodies, `ast.dump`-shape),
184
+ Apple M3 Pro (6 P + 6 E cores). `pairs/s` = pairwise `ratio` decisions per second. Full methodology,
185
+ per-repo tables, and the C++ / Python harnesses are in [`benchmarks.md`](benchmarks.md).
186
+
187
+ **Clustering throughput** — the production path (prebuilt automaton + threshold early-exit + rayon),
188
+ `cargo run --release --features bench --bin bench`:
189
+
190
+ | repo | raw ratio, 1 thread | threshold @0.5, 1 thread | threshold @0.5, 12 threads |
191
+ |---|---|---|---|
192
+ | django | 24.3k pairs/s | 117k pairs/s | **936k pairs/s** |
193
+ | sympy | 23.6k | 97.9k | **828k** |
194
+ | ha | 21.5k | 69.6k | 468k |
195
+ | mypy | 14.1k | 60.6k | 271k |
196
+ | transformers | 3.5k¹ | 47.3k | 362k |
197
+
198
+ ¹ transformers' model code has unusually long functions (per-pair RO is `O(L·log L)`), so raw
199
+ throughput is lower — the same reason `difflib` struggles there.
200
+
201
+ ### Clustering, 0.4.0: 3.7× less CPU on the name-gated workload, every byte the same
202
+
203
+ `cluster_canonicals` is what [find-dup-defs](https://github.com/prostomarkeloff/find-dup-defs)
204
+ calls once per same-name group — thousands of calls per repository, most of them two or three
205
+ long canonical bodies, a few of them hundreds. Replaying every call of one such run (2 892 calls,
206
+ 9 527 strings, 23 M characters, threshold 0.5) through the library alone, before and after:
207
+
208
+ | | 0.3.5 | 0.4.0 |
209
+ |---|---:|---:|
210
+ | single thread | 9.78 s | **2.66 s** |
211
+ | 12 threads (M3 Pro, 6 P + 6 E) | 1.30 s | **0.38 s** |
212
+
213
+ In the tool itself that pass went from 1.47 s to 0.55 s on that run (1.16× on the whole run,
214
+ whose other passes do not touch this crate). Ten output modes of that tool were diffed against
215
+ the 0.3.5 build on two corpora, and nothing moved; the library's own gates (`fast_matches_reference`, `qualifies_matches_ratio_threshold`,
216
+ the GPU parity test) pass unchanged. What changed, in the order it mattered:
217
+
218
+ - **Only the automata that are scanned get built.** A pair scans one string against the other's
219
+ automaton, so a string that is never the automaton side of a surviving pair never needs one —
220
+ in a group of two that is one build, not two, and the build is the most expensive step per string.
221
+ - **Only spanning edges are tested.** Single-linkage needs the connected components, not every
222
+ edge: candidates are visited most-similar-first in batches with a union-find between them, and a
223
+ pair already connected is not tested. A dense cluster of `k` strings costs about `k` edge tests
224
+ instead of `k²/2`, and its chained non-edges are never rejected the hard way.
225
+ - **The cluster minimum runs under a shared cap.** Every intra pair is computed against the
226
+ cluster's running minimum: a single common substring proves a pair above it before the
227
+ recursion starts (often before the scan — a common prefix or suffix is checked first), the
228
+ pair that *is* the minimum is always computed exactly, and the result is the exact minimum.
229
+ - **Narrow windows skip the automaton.** Half the recursion's windows have a `b` side of a
230
+ handful of characters; the chain walk climbs far there and mostly finds nothing, so those go
231
+ to a direct row-by-row comparison. Wider windows are taken largest-first, which closes both
232
+ early exits in fewer windows.
233
+ - **The automaton got cheaper to build and to walk:** a per-thread builder with four inline
234
+ transitions per state, endpos ranges laid out by two length-order passes instead of a tree walk,
235
+ sorted copies only for the few states with large endpos sets instead of a merge-sort tree over
236
+ every position, and a 32-byte scan slot whose inline transition continues along `b` so a match
237
+ that keeps extending costs one load per character.
238
+
239
+ Things that were measured and did not pay: a non-overlapping-interval bound on `fmatch` as an
240
+ in-scan reject (never fires on code — dissimilar functions still share long substrings), a
241
+ per-position window bound carried down the recursion, interleaving several scans to overlap
242
+ their cache misses (the scan is instruction-bound, not latency-bound), and a 128-bit character
243
+ set in the scan slot.
244
+
245
+ **vs other exact-RO implementations** — single thread, same metric, across the five repos above:
246
+
247
+ | competitor | difflib-fast speedup |
248
+ |---|---|
249
+ | Python stdlib `difflib` (pure Python — the original) | **245–1070×** |
250
+ | C++ `duckie/difflib` (well-optimized b2j) | **1.4–3.4×** |
251
+ | CyDifflib (Cython `difflib`) | **~3–10×** |
252
+ | `difflib` (Rust `difflib` crate) | 11–50× |
253
+ | `gestalt_ratio` (Rust Ratcliff–Obershelp crate) | 18–96× |
254
+
255
+ Against the **original** Python `difflib` it's a different universe: pure-Python `SequenceMatcher`
256
+ manages 15–77 exact-RO ratios/s on this code, so difflib-fast is **245–1070× faster single-threaded —
257
+ and ~1600–6600× on all 12 cores.** CyDifflib has no in-process parallelism (GIL-bound, no `nogil` /
258
+ batch API), so on all 12 cores — difflib-fast (rayon) vs CyDifflib (multiprocessing) on the same
259
+ qualifying-pairs task — the gap is **60–242×**. CyDifflib's *default* `autojunk=True` is faster but differs from the exact ratio on ~100%
260
+ of these pairs (mean |Δ| ≈ 0.22): a different metric, not a drop-in. The libraries that beat
261
+ difflib-fast on raw speed (RapidFuzz, strsim) likewise compute a *different* metric (Indel /
262
+ Levenshtein), not `difflib`'s.
263
+
264
+ ---
265
+
266
+ ## Python package
267
+
268
+ Swap `difflib.SequenceMatcher(...).ratio()` for `difflib_fast.ratio(...)` and the **same number** comes
269
+ back — hundreds of times faster per call, and **thousands of times** faster when you score a whole
270
+ corpus. That's the entire change: `import difflib_fast`.
271
+
272
+ **No PyPI** — `pip install difflib-fast` won't work. Two ways to install:
273
+
274
+ **1. Prebuilt wheel — no Rust toolchain needed.** The wheels are `cp39-abi3`, so one wheel per platform
275
+ works on **every CPython ≥ 3.9, including 3.14**. GitHub Releases isn't a package index, so pip can't
276
+ pick the wheel for you — grab the one for your platform from the
277
+ [Releases page](https://github.com/prostomarkeloff/difflib-fast/releases/latest) and install it by URL:
278
+
279
+ ```bash
280
+ # macOS Apple Silicon — swap the filename for your platform (see below):
281
+ pip install https://github.com/prostomarkeloff/difflib-fast/releases/download/v0.3.5/difflib_fast-0.3.5-cp39-abi3-macosx_11_0_arm64.whl
282
+ ```
283
+
284
+ | platform | wheel suffix |
285
+ |---|---|
286
+ | macOS Apple Silicon | `…-macosx_11_0_arm64.whl` |
287
+ | macOS Intel | `…-macosx_10_12_x86_64.whl` |
288
+ | Linux x86_64 | `…-manylinux_2_17_x86_64.manylinux2014_x86_64.whl` |
289
+ | Linux aarch64 | `…-manylinux_2_17_aarch64.manylinux2014_aarch64.whl` |
290
+ | Windows x64 | `…-win_amd64.whl` |
291
+
292
+ **2. From source — needs a Rust toolchain** (pip drives maturin automatically, no manual build):
293
+
294
+ ```bash
295
+ pip install git+https://github.com/prostomarkeloff/difflib-fast
296
+ ```
297
+
298
+ ```python
299
+ import difflib_fast
300
+
301
+ # one pair → one float, byte-for-byte difflib (autojunk=False)
302
+ difflib_fast.ratio("the quick brown fox", "the quick brown dog") # 0.8947368421052632
303
+
304
+ # cluster a corpus (single-linkage, exact min pairwise ratio per cluster)
305
+ difflib_fast.cluster_canonicals(["def f(a): ...", "def f(x): ...", "other"], 0.5)
306
+ # → [([0, 1], 0.86…)]
307
+ ```
308
+
309
+ ### All cores, no contention
310
+
311
+ `ratio` is **overloaded**: hand it a *list of pairs* and it returns a list of ratios, computing them
312
+ **in parallel across every core inside Rust** — with the GIL released. You don't touch a
313
+ `ThreadPoolExecutor`, you don't fight the GIL; you just pass the batch:
314
+
315
+ ```python
316
+ pairs = [(a, b) for a in corpus for b in corpus]
317
+ difflib_fast.ratio(pairs) # list[float], one per pair — fanned out over all cores
318
+ difflib_fast.ratio(pairs, threads=4) # …or cap it to 4 workers for this call
319
+ ```
320
+
321
+ By default it uses every core; pass `threads=N` to any batch call (`ratio(pairs, …)`,
322
+ `cluster_canonicals(…)`) to cap the pool for that call, or set `RAYON_NUM_THREADS` to change the
323
+ process-wide default with no code. Thread count never changes the result — only the speed.
324
+
325
+ This matters because Python *can't* parallelize the stdlib version: `difflib` in a `ThreadPoolExecutor`
326
+ stays GIL-bound — **23 → 23 pairs/s, zero speedup**. The batch form sidesteps that entirely: the
327
+ parallelism lives in Rust, not in Python threads, so it just scales (numbers up top; full harness in
328
+ [`benchmarks/bench_python.py`](benchmarks/bench_python.py), measured on real canonicalized Python,
329
+ M3 Pro, 12 threads).
330
+
331
+ Clustering wins biggest because each string's automaton is built **once** and reused across the whole
332
+ `n²` join (dissimilar pairs early-exit) — it's not 12× the per-call speed, it's a different algorithm.
333
+
334
+ The package is **typed** (`py.typed` + `.pyi` stubs — pyright/mypy see the overloads), gated behind the
335
+ `python` cargo feature so the pure-Rust crate keeps **zero** Python dependency by default. Built with
336
+ [maturin](https://github.com/PyO3/maturin) (mixed layout: compiled `_difflib_fast` +
337
+ `python/difflib_fast/` package); build locally into a venv with `maturin develop --release --features python`.
338
+
339
+ ### GPU from Python (macOS)
340
+
341
+ The **macOS wheels ship the Metal GPU path**. Use the `Rationer` handle — its `cluster_canonicals`
342
+ runs on the GPU when the group is large enough to pay for the dispatch, otherwise CPU; same
343
+ byte-for-byte answer either way:
344
+
345
+ ```python
346
+ import difflib_fast as df
347
+
348
+ r = df.Rationer(concurrency="gpu+cpu") # "cpu" | "gpu" | "gpu+cpu" (default)
349
+ r.concurrency # "gpu+cpu" if Metal came up, else "cpu"
350
+ r.cluster_canonicals(corpus, 0.6) # GPU-accelerated (~1.1–1.4× on Apple Silicon)
351
+ r.ratio_many(pairs) # CPU (the GPU offload loses here)
352
+ ```
353
+
354
+ On Linux/Windows wheels, or with no Metal device, a `Rationer` transparently runs everything on CPU.
355
+ Build a GPU wheel locally on macOS with
356
+ `maturin develop --release --features python,gpu` (the CLI `--features` **replaces** the pyproject
357
+ default, so list both).
358
+
359
+ ---
360
+
361
+ ## Also: exact cosine similarity join (`simjoin`)
362
+
363
+ The same "exact, or it's a bug" discipline, pointed at a different metric. **`simjoin`** is an exact
364
+ all-pairs **weighted-cosine** similarity join over sparse non-negative vectors — *every* pair with
365
+ `cos ≥ t`, no LSH, no approximation — on the provably-SOTA **L2AP** algorithm (inverted index +
366
+ Cauchy–Schwarz prefix pruning; Anastasiu & Karypis, ICDE'14). It's the principled exact replacement for
367
+ "shingle candidates → verify" near-duplicate detection: documents = functions, dimensions = canonical
368
+ lines, weights = IDF — i.e. **exact Type-3 code-clone detection**.
369
+
370
+ ```python
371
+ import difflib_fast as df
372
+
373
+ # documents as token lists → TF-IDF in Rust → every pair with cosine ≥ 0.8
374
+ docs = [["def _fn(_v0):", "return _v0 + 1"],
375
+ ["def _fn(_v0):", "return _v0 + 1"], # an exact clone of doc 0
376
+ ["import os", "import sys"]]
377
+ df.cosine_join(docs, 0.8) # → [(0, 1, 1.0)] tuples are (j, i, cos), j < i
378
+ df.cosine_join(docs, 0.8, "gpu") # same join, the dot-products run on the Metal GPU
379
+ ```
380
+
381
+ Three backends, one argument (`concurrency=`) — all auto-parallel across every core (rayon, GIL
382
+ released, exactly like `ratio`):
383
+
384
+ | `concurrency` | how | result |
385
+ |---|---|---|
386
+ | `"cpu"` | L2AP on all cores | exact `f64` |
387
+ | `"gpu+cpu"` | CPU prunes ~99% of candidates, GPU verifies the rest (f32 filter), CPU re-scores survivors exactly | **byte-identical to `"cpu"`** |
388
+ | `"gpu"` | CPU prunes, GPU verifies, emit the f32 score | ε-exact (≤ 1 differing pair per **millions**) |
389
+
390
+ On the real **top-300 PyPI** corpus (287,408 functions, 3.1M clone pairs found) the verify is
391
+ memory-**bandwidth**-bound, and the Apple GPU's memory-level parallelism wins it: **53 GB/s** of
392
+ random-gather sparse dot-products vs the CPU's 22 GB/s, so the GPU backends run the whole join
393
+ **~1.8–2× faster than the (already L2AP-tuned) CPU**, byte-for-byte. Brute force would be ~4·10¹⁰ pairs
394
+ (hours); this is seconds. `CosineJoiner(docs)` is the stateful handle (build corpus + GPU upload once,
395
+ sweep thresholds); full numbers in [`benchmarks.md`](benchmarks.md#6-similarity-join-simjoin).
396
+
397
+ In Rust: `difflib_fast::simjoin::{Corpus, cosine_join, cosine_join_with, CosineJoiner}` (GPU backends
398
+ behind the `gpu` feature). Same correctness gate as the rest of the crate — the indexed join is
399
+ asserted **bit-identical to an O(n²) brute-force oracle** on hundreds of fuzzed corpora.
400
+
401
+ ---
402
+
403
+ ## How it works
404
+
405
+ The metric is **Ratcliff–Obershelp** (Ratcliff & Obershelp, 1988) computed over a **suffix automaton**
406
+ (Blumer et al., 1985) via **matching statistics** (Chang & Lawler, 1994); short, diverse inputs take a
407
+ lighter `b2j` path instead, chosen per-input by a cheap work estimate. The composition — exact
408
+ byte-for-byte RO this way, tuned for an all-pairs clustering join — is original to this crate.
409
+
410
+ [`src/gestalt.rs`](src/gestalt.rs) is the engine: its module doc and inline comments cover the
411
+ automaton, the endpos range structure that lets one prebuilt automaton serve the whole RO recursion,
412
+ the threshold-engine early-exit math, and the measured performance floor.
413
+
414
+ ---
415
+
416
+ <div align="center">
417
+
418
+ **Exact `difflib`. None of the wait.**
419
+
420
+ Made with ⚡ by [@prostomarkeloff](https://github.com/prostomarkeloff)
421
+
422
+ </div>
423
+
@@ -0,0 +1,8 @@
1
+ difflib_fast/__init__.py,sha256=yZJOYXa8slD_DX48TM36N_rrxklqHcDaiIY_YyVAamQ,1960
2
+ difflib_fast/__init__.pyi,sha256=U31nLrTFPWLsheynjFK5qmhAULNUsY0KiN0L2FEpimE,6819
3
+ difflib_fast/_difflib_fast.pyd,sha256=OvK1JEHaJ0F5Xzz3zUMtEBlwIyu2Fjfu0J7KdnVTaiY,906240
4
+ difflib_fast/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ difflib_fast-0.4.0.dist-info/METADATA,sha256=Kqsf6hat3fLOq3Rg3HP9C6Ve0C9A1sMDvDfQtkOllC4,20839
6
+ difflib_fast-0.4.0.dist-info/WHEEL,sha256=xe4_tbg8wYdeh4O7nLbnWckzIQtCR4nODDgwke_TKy8,95
7
+ difflib_fast-0.4.0.dist-info/sboms/difflib-fast.cyclonedx.json,sha256=dpDS3Mqde-oc9VarKoaP0utq6LTGCQe46PnrjlmQwVE,23481
8
+ difflib_fast-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.15.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64