perf(parsing): allocation-free Reader.matches(), first measured pass

matches() ran on every scan decision and materialized an Array of the
literal per call, the hottest comparison in the parse profile. It now
walks the literal directly. Parse time on the 4.2 MB benchmark corpus
drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x); profile
findings and the optimization log recorded in docs/benchmarks.md.
This commit is contained in:
Mihaela Mihaljevic 2026-06-10 18:21:08 +02:00
parent 8e1415aa64
commit 0ce59c826b
3 changed files with 33 additions and 5 deletions

View File

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Scanner-side validity findings are now located structured values (validation-framework audit follow-up): `DocumentType.validityFindings` carries `ValidityFinding` values (reason plus the declared name they are about) instead of strings, `DTDSchema.declarationErrors` holds ready `ValidationError`s, and every declaration-level finding renders at its declaration's coding path (`x`, `r/@a`) instead of "at root of document", matching the XSD consistency-errors precedent. Finding texts are unchanged.
### Changed
- First measured performance pass (#139): Reader.matches() no longer materializes an Array of the literal per call (it runs on every scan decision and was the hottest comparison in the parse profile); parse time on the 4.2 MB benchmark corpus drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x). The benchmark harness, baseline, profile findings, and the optimization log live in docs/benchmarks.md.
### Fixed
- Stylesheet composition cycles no longer crash (#130 critic loop): an include or import cycle (a.xsl includes b.xsl includes a.xsl) recursed without bound and killed the host with a stack overflow, a denial of service reachable from stylesheet data. The compile chain now tracks the active load chain by resolved href and drops re-entry, while diamond composition (two units legitimately loading the same sheet) keeps loading both copies with their own import precedences, per 2.6.2. id() also gains a per-document index built on first use instead of re-walking the whole document per call, matching key(). Cycle and diamond regression tests added.

View File

@ -112,11 +112,16 @@ extension PureXML.Parsing {
/// True if the upcoming characters equal `literal`, without consuming.
mutating func matches(_ literal: String) -> Bool {
let target = Array(literal)
ensure(target.count)
guard buffer.count >= target.count else { return false }
for index in target.indices where buffer[index] != target[index] {
return false
// No per-call Array materialization: literals are short constants
// and this runs on every scan decision (the hottest comparison in
// the parse profile).
let count = literal.count
ensure(count)
guard buffer.count >= count else { return false }
var index = 0
for character in literal {
if buffer[index] != character { return false }
index += 1
}
return true
}

View File

@ -20,6 +20,25 @@ cross-implementation correctness check.
These are the honest starting numbers the #139 optimization passes burn
down; the table updates as passes land, never silently.
## Optimization log
| pass | change | parse | xpath | serialize |
|---|---|---|---|---|
| baseline | - | 0.494 s (30x) | 0.413 s (37x) | 0.061 s (5.5x) |
| 1 | allocation-free Reader.matches() | 0.414 s (26x) | unchanged | unchanged |
## Profile findings (first sample, parse path)
Top of stack, release build: String.Iterator.next (the per-character pull
closure chain), _stringCompareWithSmolCheck (Character equality in
matches/peek), String._uncheckedFromUTF8 and _allASCII (small-string
materialization), bridgeObjectRelease/swift_release (ARC on Character
buffers), UnicodeScalarView.distance. The Reader's architecture: one
closure call per scalar, [Character] lookahead, Character-typed
classification. The structural fix is a byte-level (UTF-8) scanner with
scalar classification tables; that is its own #139 pass, designed, not
patched.
## Known structure of the gap
- Parse and XPath dominate; serialize is closest.