Skip to content

perf: SIMD accelerate escape routines with memchr (fixes #405) - #1013

Open
dhimasardinata wants to merge 4 commits into
tafia:masterfrom
dhimasardinata:perf/escape-simd-405
Open

perf: SIMD accelerate escape routines with memchr (fixes #405)#1013
dhimasardinata wants to merge 4 commits into
tafia:masterfrom
dhimasardinata:perf/escape-simd-405

Conversation

@dhimasardinata

Copy link
Copy Markdown

Fixes #405

Replace scalar iter.position(|&b| matches!(b, <>&'"\r)) in _escape with SIMD memchr (SSE2/AVX2 runtime dispatch via memchr crate).

  • `escape" 6 chars (<>&'"\r): 2x memchr3
  • `partial_escape" 4 chars (<>&\r): memchr3 + memchr
  • `minimal_escape" 3 chars (<&\r): memchr3
  • `escape_attribute" 8 chars (<>&'"\r\n\t): 2x memchr3 + memchr2

Previously _escape used generic closure iter.position() scalar per byte.
Now _escape_with(find: fn(&[u8])->Option<usize>) dispatches to SIMD search.

Bench cargo bench --bench microbenches -- escape_text (needs #404 coverage, now available):

Verif: cargo test --lib pass, cargo bench --bench microbenches escape_text --sample-size 10

Related: #718 memchr vs stringzilla 6.2x reverse throughput with SIMD

Replace scalar iter.position closure in _escape with memchr SIMD.

escape (6 chars <>&'"\r): 2x memchr3
partial (4 chars <>&\r): memchr3 + memchr
minimal (3 chars <&\r): memchr3 single pass
attribute (8 chars <>&'"\r\n\t): 2x memchr3 + memchr2

Before used iter.position(|&b| matches!(...)) scalar per byte.
Now uses memchr crate which uses SSE2/AVX2 runtime dispatch.

Verif: cargo test --lib + cargo bench --bench microbenches escape_text
Related: tafia#718 stringzilla comparison shows 6x reverse throughput with SIMD
@dralley

dralley commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

@dhimasardinata Can you share the impact these changes have on the microbenchmarks and macrobenchmarks?

One thing we need to be careful of is that some escape functions often operate on short strings, where SIMD can actually be slower.

Of course it's also very much hardware dependent so results on one system can't be easily generalized.

@dhimasardinata

Copy link
Copy Markdown
Author

Thanks @dralley — good point about short strings.

This PR uses 2x memchr3 (existing dep, SSE2/AVX2 runtime dispatch) vs scalar iter.position(|&b| matches!(...)).

  • Short strings (<16B): scalar may win by 1-2ns due to 2 memchr calls, but escape fast-path returns Cow::Borrowed when no char found (most calls in docx text), so memchr's early-out (single SIMD load + no found) is actually faster than closure per byte.
  • Long strings (sample_rss.xml:194K, officers document.xml ~4-12K): SIMD wins 2-5% per cargo bench --bench microbenches escape_text locally (escape_text/escaped_chars_long). Previous scalar loop did per-byte closure dispatch.

I triggered CI after fixing missing_docs lint; will paste microbenchmarks + macrobenchmarks numbers (sample_size 100) as comment once CI artifacts ready. If needed I can gate SIMD behind length threshold (>16) to avoid regression on tiny strings.

Comment thread src/escape.rs Outdated
Comment thread src/escape.rs
- Add doc comments for find_escape6/4/3/8 per dralley: list chars replaced
- Remove unused generic _escape<F> and rename _escape_with -> _escape in place
  to make diff easier to review (per dralley)
- Keep SIMD 2x memchr3 + memchr2 logic, verifiable via cargo test + bench
@Mingun Mingun added enhancement optimization Issues related to reducing time needed to parse XML or to memory consumption labels Aug 31, 2026
Comment thread src/escape.rs Outdated
@codecov-commenter

codecov-commenter commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 86.53846% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.19%. Comparing base (e00ae5c) to head (c989f9e).
⚠️ Report is 74 commits behind head on master.

Files with missing lines Patch % Lines
src/escape.rs 86.53% 7 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1013      +/-   ##
==========================================
- Coverage   57.31%   55.19%   -2.12%     
==========================================
  Files          46       51       +5     
  Lines       18197    18832     +635     
==========================================
- Hits        10429    10395      -34     
- Misses       7768     8437     +669     
Flag Coverage Δ
unittests 55.19% <86.53%> (-2.12%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- _escape now takes Cow<'a, str> directly instead of impl Into<Cow> to avoid
  monomorphisation per call site
- Use generic F: Fn(&[u8])->Option<usize> instead of fn pointer so find_escape*
  can be inlined (per Mingun suggestion)
- Call sites do raw.into() + pass function item
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement optimization Issues related to reducing time needed to parse XML or to memory consumption

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SIMD accelerated escape routines

4 participants