perf: skip no-op quote/paren and zero-width passes (~10% on large text) - #72
Merged
Merged
Conversation
Two behavior-preserving fast-paths that cut per-call work on short prose: - BetweenPunctuation: each quote/paren sub is anchored on its opening delimiter, so guard each base method on 'opener in txt' and return early when absent. On quote/paren-free text this turns 8 full-text regex passes into cheap membership checks (84 -> 76 re.sub per short segment()). The guard lives in each method, NOT the dispatcher, because language subclasses repurpose a base method for a different opener (German routes „…“ through sub_punctuation_between_double_quotes); a dispatcher-level check would skip that override and split inside the quote. Regression test covers exactly that German case. - _strip_zero_width_before_sentence_closers: early-out via a precompiled search when the segment has no zero-width char, skipping the char-by-char rebuild on the common clean-text path. Output is byte-identical (full suite 1970 passed). Wall-clock gain is small (the skipped regex passes were already cheap no-match scans); the win is in instruction count, which the competitive CodSpeed benchmarks track against pySBD/punkt on short and medium input.
yisding
marked this pull request as ready for review
June 14, 2026 01:59
Merging this PR will improve performance by 10.27%
Performance Changes
Tip Curious why this is faster? Comment Comparing |
yisding
pushed a commit
that referenced
this pull request
Jun 14, 2026
Multi-agent + differential-profiling investigation of the short/medium gap vs pySBD. Key finding: post-#72 there is no regression. Measured: - wall-clock: short 0.99x pysbd, medium 0.93x (parity/ahead) - instruction-count proxy (line-events): short 0.93x (we're ahead), down from 1.24x pre-#72 Root cause of the original ~28% CodSpeed gap: it was largely an instruction-count artifact of pure-Python per-char loops (counted in full by cachegrind, ~0 wall-clock), whose #1 contributor — the zero-width scanner (876 of 2787 line-events) — was gated by #72. We also run FEWER regex ops than pySBD (118 vs 266) and our Aho-Corasick is faster than a naive 'in'-loop on short input, so neither pass-count nor discovery was the cause. Rewrites the plan as a 'extend the lead' menu, tiered by improves-both vs CodSpeed-only: Tier 1 (list early-out/guards, resplit+callback gating, always-on abbreviation gating), Tier 2 (Aho-Corasick DFA delta-table + length-gated hybrid, ~2x scan), Tier 3 (glued-dot scanner C-regex, instruction-count only). Adds benchmarks/differential_profile.py.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Follow-up to the competitive benchmarks (#71), which showed our short/medium
segment()lags pySBD by ~22%. This investigated that — and the safe, behavior-preserving wins turned out to help large text, not short/medium (see CodSpeed result below).What I found (honest)
Profiling a short (87-char)
segment()shows the cost is dominated by the abbreviation pipeline (Aho-Corasick scan + per-abbreviationre.sub+ the manyapply_rulespasses), which is core engine logic and risky to touch. The clearly-wasted work was smaller:What this PR does
Two provably no-op-equivalent fast-paths:
opener in txt. Cuts 84 → 76re.subper short segment. The guard is per-method, not dispatcher-level — German repurposessub_punctuation_between_double_quotesfor„…“(opener U+201E, no ASCII"), so a dispatcher'"' in txtcheck would skip that override and split inside the quote. (I hit exactly that bug mid-development; there's now a regression test that fails on the dispatcher form.)_strip_zero_width_before_sentence_closers— early-out via a precompiled search when no zero-width char is present.CodSpeed result (the real verdict)
+10% on large text, short/medium untouched:
test_segment[large]test_should_wait_for_more[large]The removed work scales with segment count / text size (the zero-width guard runs per output segment), so it helps large text and is negligible on short. The short/medium gap vs pySBD is structural — the fixed-cost abbreviation pipeline — and is not addressed here; that needs a separate, deeper effort.
Correctness
tests/regression/test_between_punctuation_guard.pycovers the German„…“trap, English quote/paren protection, and quote-free equivalence — verified to fail on the dispatcher-level form (AssertionError) and pass on the per-method form.https://claude.ai/code/session_01RBqMKEGgvcZEAXLTPP5FiV