Skip to content

Perf: split input once with mb_str_split in the main parse loop - #60

Merged
mmucklo merged 1 commit into
masterfrom
perf/ascii-fast-path
Jul 13, 2026
Merged

Perf: split input once with mb_str_split in the main parse loop#60
mmucklo merged 1 commit into
masterfrom
perf/ascii-fast-path

Conversation

@mmucklo

@mmucklo mmucklo commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Realizes the ROADMAP main-loop performance item — and the new benchmark CI job (#59) measures it automatically.

Change

The main parse loop called mb_substr($emails, $i, 1, $encoding) for every character, plus once per character for each comment/quoted look-ahead. For multi-byte encodings mb_substr rescans from the start of the string to locate the Nth character — O(n) per call, O(n²) across the loop. This splits the input once with mb_str_split() and indexes the resulting array: a single O(n) pass.

The diff is confined to the main loop — the three mb_substr($emails, …) sites become $chars[$i] / $chars[$j].

Results (composer bench:compare vs v3.3.2 baseline, same host)

Subject Before After Δ
Simple ASCII 103.7 μs 86 μs −17%
Name-addr 137.3 μs 104 μs −24%
UTF-8 local part 130.4 μs 98 μs −25%
Obs-route 108.7 μs 81 μs −26%
Comment extraction 152.5 μs 114 μs −25%
Batch 10 682 μs 541 μs −21%
Batch 100 (stream) 13,415 μs 9,783 μs −27%

Consistent 10–27% across the suite; biggest gains on longer inputs, as expected for an O(n²)→O(n) fix.

Safety

  • Behavior-preserving: all 91 tests pass unchanged, PHPStan level 8 / Psalm / cs clean.
  • mb_str_split() is encoding-aware (same $encoding), so multi-byte handling is identical — verified by the existing UTF-8/ISO-8859-1/Shift-JIS tests.
  • $emails/$len are not mutated inside the loop, so pre-splitting is safe.

One trade-off worth noting: the character array costs memory proportional to input length (bounded by input size). For very large single batches a chunked reader would cap that — noted as a ROADMAP follow-up.

The main loop called mb_substr($emails, $i, 1, $encoding) for every character,
plus for each comment/quoted look-ahead. For multi-byte encodings mb_substr
rescans from the start of the string to locate the Nth character — O(n) per
call, O(n^2) across the loop. Splitting once with mb_str_split() and indexing
the resulting array is a single O(n) pass.

~10-27% faster across the benchmark suite (composer bench:compare vs the v3.3.2
baseline), largest gains on longer inputs: batch 100 stream 13,415us -> 9,783us
(-27%), name-addr -24%, obs-route -26%, comment extraction -25%. No behavioral
change — all 91 tests pass unchanged.
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.07%. Comparing base (f109878) to head (30a664d).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master      #60      +/-   ##
============================================
- Coverage     93.17%   93.07%   -0.10%     
  Complexity      380      380              
============================================
  Files             6        6              
  Lines           981      982       +1     
============================================
  Hits            914      914              
- Misses           67       68       +1     
Files with missing lines Coverage Δ
src/Parse.php 90.36% <100.00%> (-0.14%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mmucklo
mmucklo merged commit 30b9e9a into master Jul 13, 2026
14 checks passed
@mmucklo
mmucklo deleted the perf/ascii-fast-path branch July 13, 2026 06:52
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