Skip to content

Third round of libse micro-optimizations (BenchmarkDotNet-verified)#12541

Merged
niksedk merged 1 commit into
mainfrom
perf/libse-micro-optimizations-3
Jul 16, 2026
Merged

Third round of libse micro-optimizations (BenchmarkDotNet-verified)#12541
niksedk merged 1 commit into
mainfrom
perf/libse-micro-optimizations-3

Conversation

@niksedk

@niksedk niksedk commented Jul 16, 2026

Copy link
Copy Markdown
Member

Same method as #12530/#12533. Three optimizations, each with A/B output-identity proof and BenchmarkDotNet numbers (.NET 10, Apple M4).

1. Hoist 15 method-local new Regex(...) constructions in subtitle formats — up to ~8,000×

Eight format files constructed regexes inside methods. The worst was WebVTT.RemoveNativeFormatting: three RegexOptions.Compiled regexes built on every call — compiled construction emits IL, costing ~1.7 ms before processing a single cue, on one of the most-converted formats there is. InqScribe built one regex per text line; DvSubtitle six per call; TimedText10/TimedTextNoNs, Son, DCinemaSmpte2010 and the ASSA drawing-tag helper had one or two each. All are now private static readonly ... RegexOptions.Compiled, matching the codebase convention.

Method Mean Ratio Allocated
Old (3 compiled per call, WebVTT shape) 1,738,436 ns 1.000 43,060 B
New (statics) 207 ns 0.000 0 B

2. HtmlUtil.RemoveOpenCloseTags — 1.5× faster, no-op lines allocation-free

Used per paragraph by a dozen format writers (PAC, Cavena890, TS, BelleNuit, several Unknown formats, translate formatting). The Regex.Replace + MatchEvaluator shape allocated a closure over tags, a string per match for the tag-name group, one more per kept match — and rebuilt the string even when nothing matched. Now walks matches, compares the group via span (OrdinalIgnoreCase), and returns the original instance when no tag is removed.

Method Mean Ratio Allocated Alloc Ratio
Old 1.75 µs 1.00 6.63 KB 1.00
New 1.17 µs 0.67 6.19 KB 0.93

(The alloc delta looks modest because the mixed benchmark is dominated by removal-heavy lines; untouched lines drop from full-rebuild to zero.)

3. StringExtensions.ToRtfPart — 1.6× faster, −52% allocations

sb.Append("\\u" + Convert.ToUInt32(c) + "?") allocated two strings per non-ASCII character — painful on CJK/Cyrillic text in the RTF format/copy paths. Now appends the parts separately.

Method Mean Ratio Allocated Alloc Ratio
Old 1.90 µs 1.00 15.36 KB 1.00
New 1.21 µs 0.64 7.36 KB 0.48

Correctness

  • RemoveOpenCloseTags: 30,000 randomized tag-soup inputs (spaced tags < / i >, unclosed <unclosed, case variants, 5 < 6 non-tags, <v Speaker Name>) across five tag sets — byte-identical vs the old implementation, plus a reference-equality check on the new no-change fast path.
  • ToRtfPart: 30,000 random inputs mixing RTF-special chars (\, {, }), CRLF, and CJK/Cyrillic/Greek — byte-identical.
  • Regex hoists: pattern strings copied verbatim; behavior covered by the format round-trip tests.
  • All 485 libse + 233 seconv tests pass.

🤖 Generated with Claude Code

- Subtitle formats: hoist 15 method-local Regex constructions to static
  compiled fields (WebVTT, InqScribe, DvSubtitle, TimedText10,
  TimedTextNoNs, Son, DCinemaSmpte2010, AdvancedSubStationAlpha). Worst
  offender was WebVTT.RemoveNativeFormatting, which built three COMPILED
  regexes on every call - compiled construction emits IL and cost ~1.7 ms
  per conversion before a single cue was processed (benchmarked ~8000x
  faster with statics). InqScribe built one per text line.

- HtmlUtil.RemoveOpenCloseTags: walk matches and compare the tag-name
  group in place instead of Regex.Replace with a MatchEvaluator closure -
  the old shape allocated a closure over the tags array, a string per
  match for the group and one per kept match, and rebuilt the string even
  when nothing matched. Now returns the original instance when no tag is
  removed. Runs per paragraph in a dozen format writers. 1.5x faster on a
  mixed workload.

- StringExtensions.ToRtfPart: append the "\u<code>?" escape in parts
  instead of string concatenation, which allocated two strings per
  non-ASCII char - noticeable on CJK/Cyrillic subtitles. 1.6x faster,
  half the allocations.

Verified: BenchmarkDotNet on all three; A/B sweeps proving byte-identical
output vs the old implementations (30k randomized tag-soup inputs across
five tag sets incl. spaced/unclosed/case variants, 30k random RTF inputs
incl. RTF-special chars and non-ASCII), plus a reference-equality check
for the new no-change fast path. All 485 libse + 233 seconv tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@niksedk
niksedk merged commit 15fb7f2 into main Jul 16, 2026
1 of 3 checks passed
@niksedk
niksedk deleted the perf/libse-micro-optimizations-3 branch July 16, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant