-
Notifications
You must be signed in to change notification settings - Fork 0
0043 the equality table is sized to the pattern
Status: accepted · Date: 2026-08-20 · Amends: 0004
Amends two of 0004's backlog bullets — Lift the Latin-1 restriction and the equality table's fixed cost — and retires both. The rest of that record stands.
The bit-parallel kernels index a 256-entry table by the character. Two costs follow, and 0004 proposed one fix for both: "a sparse or hashed table would generalise it".
-
The table is zeroed on every call.
stackalloczeroes it because nothing disableslocalsinit, so a call whose work isO(n)pays 2 KB of memset for a table of which at most 64 entries are used. - A pattern above U+00FF cannot be represented in it, so CJK and emoji fell back to the dynamic program in the UTF-16 mode.
Generalising the whole table is refused. That is what the code-point path already does, and #208 measured its price: renaming both operands through a probe table makes it cross the dynamic program at a pattern of 10 where the character path crosses at 8. The table is instead sized to what a pattern actually needs, in two independent ways.
The table is held between calls in one kernel and not the other. A
[ThreadStatic] table kept all-zero, restored by walking the pattern, costs O(m)
rather than O(256). Swept over the pair corpus at a longest-held pattern of 0, 16,
32 and 64, it is worth 13% of the length-32 bucket on Indel and a regression at every
value on Levenshtein, so BitParallelLcs holds its table at 32 and Myers keeps its
stackalloc. The LCS recurrence is four operations per text character against Myers'
dozen, so the identical fixed cost is a far larger share of what its call does.
The dense table keeps Latin-1 and a side table carries the rest. Out-of-range
characters are the rare case, so the common path is unchanged and only a pattern that
needs the side table pays for it. In the single-word kernels the side table is a
separate method's stackalloc; in the blocked ones its symbols are extra rows of
the existing table, (256 + slots) × blocks words with slot k at row 256 + k, so
the multi-word carry and borrow are untouched.
-
A
stackalloczeroes on entry to the method holding it, taken branch or not. That is why each wide path is its own method: otherwise a Latin-1 pattern would be charged the side table on every call. It also forbids the tidier shapes — one method choosing its table with a conditional cannot work. - The held table's invariant is restored on every exit, the refusal included. A pattern is written character by character and abandoned partway when one leaves Latin-1, so entries are already set when the kernel gives up; the damage shows on the next call, whose text reads a mask its predecessor left behind.
- The side table's capacity is bounded by the pattern, not by a constant. 128 slots are sound for one word only, 64 characters holding at most 64 distinct symbols. A blocked pattern has no length bound, so the table can fill and leave the probe no free slot to stop on.
- A microbenchmark sized the first change and got its sign wrong for one kernel. It rotated 64 strings, a working set small enough that the held table never left L1; a corpus walk evicts it. Sizing a fixed cost in isolation is worth doing first and is not worth believing alone.
- Whether the added branch costs anything on the Latin-1 path is a question about generated code, and was answered by diffing the JIT's output rather than by timing a loaded machine: identical, all 83 instructions.
Numbers, with their machine and their window, in
../guides/performance.md.
- 0001-target-framework
- 0002-unicode-comparison-unit
- 0003-provenance-and-licensing
- 0004-levenshtein-myers-backlog
- 0005-hamming-jellyfish-divergence
- 0006-ratcliff-autojunk
- 0007-metaphone-scope
- 0008-italian-enza-nltk-divergence
- 0009-sample-consumes-a-local-feed
- 0010-stop-word-list-provenance
- 0011-persistence-format
- 0012-per-package-versioning
- 0013-sentencepiece-parity-scope
- 0014-precompiled-normalizer
- 0015-sonar-rules-in-the-build
- 0016-metrics-package-placement
- 0017-bpe-parity-scope
- 0018-multiclass-roc-auc-parallelism-is-opt-in
- 0019-the-net-analysers-run-in-the-build-too
- 0020-normalize-is-a-projection-not-a-parameter
- 0021-multioutput-is-a-method-not-an-enum
- 0022-added-token-matching-flags
- 0023-byte-level-decode-substitutes
- 0024-weighted-median-averages-within-scikit-learns-epsilon
- 0025-quickselect-replaces-a-full-sort-for-the-median
- 0026-r2-and-explainedvariance-split-their-undefined-cases-differently
- 0027-r2-and-explainedvariance-vectorize-only-a-single-output
- 0028-log1p-is-kahans-identity-not-math-log-1-plus-x
- 0029-balanced-accuracy-adjusted-is-left-to-ieee-754-at-the-edge
- 0030-cohen-kappa-keeps-scikit-learns-expected-matrix-orientation
- 0031-nosamplecorrect-mirrors-numpys-float64-upcast
- 0032-fbeta-substitutes-tp-predicted-and-support-algebraically
- 0033-compensated-sum-is-neumaiers-variant
- 0034-dropout-is-refused-for-want-of-a-user
- 0035-a-null-pre-split-is-removed-with-invert-not-isolated
- 0036-a-member-may-ship-without-an-oracle-if-it-says-so
- 0037-the-guards-run-before-the-commit
- 0038-the-gate-confronts-an-exception-tag-with-the-page-that-documents-it
- 0039-mutual-information-returns-zero-on-an-empty-input
- 0040-a-curve-is-a-sealed-class-per-curve
- 0041-one-sample-file-per-public-class
- 0042-phonetic-encoders-refuse-a-null-word
- 0043-the-equality-table-is-sized-to-the-pattern
- 0044-compression-belongs-to-the-caller
- 0045-a-console-call-carries-its-reason-on-the-line
- 0046-check-adr-immutable-runs-in-ci-only
- 0047-one-gate-per-kernel-not-one-per-alphabet
- 0048-the-gate-depends-on-the-kernel-and-the-alphabet
- 0049-two-gates-per-kernel-tested-where-the-width-is-known
- 0050-the-sentencepiece-bpe-lineage-stays-a-bpe-model
- benchmark_latest
- decisions
- equivalence
- matplotlib
- migration
- nightly_run
- numpy
- pandas
- performance
- pytorch
- seaborn
- sklearn
- statsmodels