Repository navigation
Conversation
CGI.escapeHTML checked one byte at a time and always allocated a buffer six times the input's size, even when nothing needed escaping. ERB::Util.html_escape does the same job and got a SIMD search in ruby/erb#136. Port erb's SSE2/NEON match code (originally from the json gem). Strings of 64 bytes or more are searched for the first ' & < > ", and a string with none is returned by rb_str_dup with no buffer. Otherwise the clean prefix is copied at once, clean 16-byte blocks are copied whole, and the other bytes are escaped 4 at a time. Strings under 64 bytes skip the search, since it costs more than it saves when they need escaping. The 4-byte step replaces a byte loop because gcc and clang each compiled some forms of that loop into code slower than the old one. escape_html_into is NOINLINE so that with _FORTIFY_SOURCE=3 its small memcpy calls stay plain moves instead of becoming __memcpy_chk calls. Without SIMD (CGI_DISABLE_SIMD, MSVC, other CPUs) the search checks 8 bytes per step and is skipped under 128 bytes. extconf.rb uses erb's compile check to define CGI_ENABLE_SIMD. Output, encoding and the "always a new, unfrozen String" behaviour are unchanged.
ptweezy
force-pushed
the
simd-escape-html
branch
from
November 3, 2026 04:09
cbbca0d to
549df05
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CGI.escapeHTMLchecks one byte at a time and always allocates a buffer six times the input's size, even when nothing needs escaping. This ports the SSE2/NEON search from ruby/erb'sERB::Util.html_escape(ruby/erb#136 by Jean Boussier and the NEON follow-ups by Scott Myron, originally from the json gem) and builds the escaping around it:',&,<,>or". A string with none is returned as a copy, with no buffer allocated.Without SIMD (
CGI_DISABLE_SIMD=1, MSVC, other CPUs), the search checks 8 bytes per step and is skipped under 128 bytes.extconf.rbuses erb's compile check.Benchmarks
I timed 52 cases: ruby/ruby's
benchmark/cgi_escape_html.yml, clean strings from 16 bytes to 8 KB, strings with a<every 8 to 128 bytes, dense and random text, markup snippets such as<a href="...?x=1&y=2">Link</a>, and strings of 0 to 31 bytes. Each run also timed a second build of the current code to measure noise. Numbers are speed relative to the current code, best of 5 runs (best of 25 under 32 bytes, and best of 3 interleaved rounds of 3 on GitHub's runners).On each machine, the slowest case is faster than the current code or within noise:
CGI_DISABLE_SIMD=1_FORTIFY_SOURCE=2_FORTIFY_SOURCE=3¹
"", in a table where the second build of the current code also read 0.982x on that row. Other runs give 1.02x to 1.03x.²
"abcde", in a table where both builds of the current code ran about 5% faster than in any other run. At best of 25 it is 1.03x and 1.02x.³
clean_31. The same machine code was 1.033x or faster on every case in the previous run on that runner.GitHub had no macOS arm64 runner available for the final version. An earlier version with the same NEON code passed the tests on Apple M1 and was 1.08x or faster in every case.
Selected cases on the Ryzen:
CGI_DISABLE_SIMD=1CGI_DISABLE_SIMD=1long_no_escape(1500 bytes, clean)long_all_escape(50 bytes, all special)example_html"abcde"<per 128 bytes<per 32 bytes<per 8 bytes<b>bold</b><a href="/path/to/page?x=1&y=2">Link</a>"gcc" means
-D_FORTIFY_SOURCE=2. With Ubuntu's default_FORTIFY_SOURCE=3, the current loop'smemcpybecomes a__memcpy_chkcall, and the first three rows rise to 27.3x, 1.95x and 4.22x.Testing
rake testpasses for the SIMD andCGI_DISABLE_SIMD=1builds on x86-64 and arm64 Linux, with gcc and clang.This covers what #85 does (no buffer for clean strings, clean runs copied at once), so the two conflict. I can rebase onto #85 if you'd rather merge it first, or close this if you prefer the smaller change.
A follow-up could add a
CGI_DISABLE_SIMD=1CI entry, like ruby/erb#140, to keep both paths tested.