Repository navigation
Speed up tokenization ~25%, add perf bench, clean up lint and the stale gold file - #64
Merged
Merged
Conversation
Ruff 0.16 enabled pyupgrade, RUF, SIM and other rule families by default, which the `ruff>=0.15` lint dependency picks up: 213 findings in unchanged code (CI runs only mypy and pytest, so it stayed green). - Apply ruff's safe autofixes: PEP 585/604 annotations (`list[str]`, `X | None`) and `collections.abc` imports, valid since the 3.10 floor. - Keep the bound TypeVar on `Tok.from_txt`/`from_token` instead of the autofix's `typing_extensions.Self`: Tokenizer has no runtime dependencies and `typing.Self` needs Python 3.11. - Annotate the `Abbreviations` class tables as `ClassVar`, use tuple `startswith`/`endswith`, `yield from`, implicit format fields, and `datetime.date` for date validation. - `noqa` FURB166: its suggested `base=0` would break `&#x...;` escapes. - Ignore SIM102: nested ifs mirror the token matching logic, and several sit in elif chains where collapsing them would change behaviour. - Update CLAUDE.md: the 3.9 annotation advice was stale. No behaviour change: token output digests on the large test set are identical, and clean installs on CPython 3.10 and PyPy 3.11 work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiling with the CPython 3.15 sampling profiler (`profiling.sampling`) showed two low-hanging hot spots in the raw token stages: - `zerowidth_replacement` and `composite_replacement` ran a regex scan on every raw token, although such characters are rare. Check once per text instead, and skip the per-token passes when none are present (a removed zero-width character can bring a composite glyph together, so its presence also enables the composite pass). ~10%. - `parse_digits` tries up to ~25 `re.match(<pattern string>, w)` calls per numeric token, each paying ~220 ns of `re` cache lookup before matching. Precompile all string-pattern regexes in tokenizer.py at module level, and use `str.replace` for the literal `re.sub` calls. ~12%. Token output is byte-identical (digests of kind, txt, val, original and origin_spans over the large test set). Speedup on the large test set: +25% on CPython 3.13, +28% on 3.14, +22% on 3.15. bench/bench.py times five workloads (tokenize with and without annotation, single-string input, split_into_sentences, original=True) and records an output digest per workload; --save/--compare report the speed change and whether output is identical. bench/profile.sh runs the 3.15 sampling profiler with --blocking, which is required here: without it the profiler fails to unwind ~70% of stacks in the generator pipeline and attributes inner stages' work to the next() calls of outer ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test/toktest_large_gold_acceptable.txt, documented in the README as the current output of the tokenize command, was last regenerated in January 2020: it is byte-identical to the output of Tokenizer 2.0.0. Releases 2.2.0, 3.0.0 and 3.4.0 changed 592 of its lines (and PR #63 one more), and nothing checked it, so the drift went unnoticed. Running each release tag's own CLI shows no output change from 3.4.0 through 3.6.4. - Regenerate the gold file from the current tokenize output. - Add test/test_large_gold.py, which fails when the CLI output for test/toktest_large.txt differs from the gold file and shows the first differing hunks. - Recompute the [input|gold] line ranges in test/Overview.txt: each gold range now covers all output lines containing text from the input lines. That rule reproduces 127 of the 137 original ranges against the old gold file; the rest were boundary inconsistencies and four typos (e.g. 167913, 18187, a reversed input range and a malformed reference). A dated note in the legend says the #B/#C descriptions date from 2.0.0. - README and CLAUDE.md: describe the test and how to regenerate the gold file after an intended tokenization change. Note that the drift is not uniformly an improvement: against toktest_large_gold_perfect.txt, current output gains 244 perfect lines and loses 215 compared with 2.0.0 (e.g. sentence-final abbreviations now keep their period, and a few sentence splits after abbreviations are missed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Three independent commits; review them one at a time.
1. Clean up ruff findings from its 0.16 default rule set
Ruff 0.16 turned on pyupgrade, RUF, SIM and other rule families by default. Because the lint group pins
ruff>=0.15, unchanged code suddenly had 213 findings. CI runs only mypy and pytest, so it stayed green.list[str],X | None,collections.abcimports (fine with the 3.10 floor).Tok.from_txt/from_tokento usetyping_extensions.Self. Tokenizer has no runtime dependencies, so that import would fail on a clean install. The tests didn't catch it becausetyping_extensionscomes in through dev dependencies. The original bound TypeVar stays, with anoqaand the reason.ClassVaron theAbbreviationstables, tuplestartswith/endswith,yield from,datetime.datefor date validation.base=0suggestion would break&#x...;HTML escapes, so that line gets anoqa.ifs in the core matching logic. Some of them sit inelifchains, where collapsing would change behavior. If you'd rather collapse the safe ones, say so.2. Speed up tokenization ~25% and add a performance bench
Found with the new CPython 3.15 sampling profiler (
python -m profiling.sampling):parse_digitsmakes up to ~25re.match("<pattern>", w)calls per number, each paying ~220 ns ofrecache lookup. All string-pattern regexes intokenizer.pyare now precompiled. (~12%)tokenize, large test setOutput is byte-identical: digests of
(kind, txt, val, original, origin_spans)for every token match across all five bench workloads.bench/bench.pyrecords timings plus output digests (--save/--compare).bench/profile.shwrites a pstats summary, self time per function, a flamegraph and a line heatmap. It uses--blockingon purpose: without it, the 3.15 profiler fails to unwind ~70% of stacks in this generator pipeline (its "Error rate" header) and charges inner stages' work to the outer stages'next()lines.On
matchstatements (looked at while doing this): on 3.15, value patterns likecase TOK.WORD:still compile to the sameCOMPARE_OP+ jump sequence as anif/elifchain, with no jump table. In a microbenchmark they were 8–14% slower on 3.13–3.15, so this PR doesn't use them.3. Regenerate the large test set gold file and enforce it with a test
toktest_large_gold_acceptable.txtwas byte-identical to Tokenizer 2.0.0's output (Jan 2020). Running each release tag's own CLI shows the drift came from 2.2.0, 3.0.0 and 3.4.0, plus one line from #63, and that nothing changed from 3.4.0 through 3.6.4.test/test_large_gold.pynow fails on any CLI output drift.[input|gold]ranges intest/Overview.txtare recomputed. The rule used reproduces 127/137 of the original ranges on the old file; the remainder were boundary inconsistencies plus four typos.Overview.txtlegend.gold_perfect, current output gains 244 correct lines and loses 215. Examples of losses: sentence-final abbreviations keep their period (ehf.instead ofehf .), and some sentence splits after abbreviations are missed (…am.k. Bara að það væru 7 .). This PR records current behavior as the baseline; it doesn't fix those.Verification
pytest: 152 passed (CPython 3.13 env and 3.10); mypy clean; ruff check + format clean🤖 Generated with Claude Code