Skip to content

Additional ARM Neon SIMD optimizations - #141

Open
samyron wants to merge 1 commit into
ruby:masterfrom
samyron:sm/neon-vtbl
Open

Additional ARM Neon SIMD optimizations#141
samyron wants to merge 1 commit into
ruby:masterfrom
samyron:sm/neon-vtbl

Conversation

@samyron

@samyron samyron commented Sep 10, 2026

Copy link
Copy Markdown

Further optimize the NEON SIMD escape path using vectorized table lookups (vqtbl4q_u8) instead of direct character compares and a logical OR reduction.

Additionally, this commit fixes a shift-by-64 undefined behavior when a match lands on the last lane of a block. Depending on where the index lands, at best the string is escaped incorrectly, at worst it cases a segmentation fault:

irb(main):001> require 'erb'
=> true
irb(main):002> corrupt = "a" * 15 + "<" + "a" * 20
=> "aaaaaaaaaaaaaaa<aaaaaaaaaaaaaaaaaaaa"
irb(main):003> actual = ERB::Util.html_escape(corrupt)
=> "aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaa"
irb(main):004> crash = "a" * 15 + ">" + "a" * 15 # 31 bytes
=> "aaaaaaaaaaaaaaa>aaaaaaaaaaaaaaa"
irb(main):005> ERB::Util.html_escape(crash)
(irb):5: [BUG] Segmentation fault at 0x00000001230affe9

This was run on a Macbook Air M1.

== 1k no matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl     1.191M i/100ms
Calculating -------------------------------------
        sm/neon-vtbl     11.896M (± 0.6%) i/s   (84.06 ns/i) -     59.546M in   5.005745s

Comparison:
              master:  9406864.5 i/s
        sm/neon-vtbl: 11895905.7 i/s - 1.26x  faster

== 1k few matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl   277.531k i/100ms
Calculating -------------------------------------
        sm/neon-vtbl      2.722M (± 1.7%) i/s  (367.34 ns/i) -     13.877M in   5.098968s

Comparison:
              master:  1485804.8 i/s
        sm/neon-vtbl:  2722269.4 i/s - 1.83x  faster

== 1k many matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl   178.565k i/100ms
Calculating -------------------------------------
        sm/neon-vtbl      1.787M (± 1.1%) i/s  (559.45 ns/i) -      9.107M in   5.095378s

Comparison:
              master:  1462148.5 i/s
        sm/neon-vtbl:  1787482.0 i/s - 1.22x  faster

…kups (vqtbl4q_u8) instead of direct character compares and a logical OR reduction.

Additionally, this commit fixes a shift-by-64 undefined behavior when a match lands on the last lane of a block. Depending on where the index lands, at best the string is escaped incorrectly, at worst it cases a segmentation fault:

```
=> true
irb(main):002> corrupt = "a" * 15 + "<" + "a" * 20
=> "aaaaaaaaaaaaaaa<aaaaaaaaaaaaaaaaaaaa"
irb(main):003> actual = ERB::Util.html_escape(corrupt)
=> "aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&ruby#39;aaaa"
irb(main):004> crash = "a" * 15 + ">" + "a" * 15 # 31 bytes
=> "aaaaaaaaaaaaaaa>aaaaaaaaaaaaaaa"
irb(main):005> ERB::Util.html_escape(crash)
(irb):5: [BUG] Segmentation fault at 0x00000001230affe9
```

This was run on a Macbook Air M1.

```
== 1k no matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl     1.191M i/100ms
Calculating -------------------------------------
        sm/neon-vtbl     11.896M (± 0.6%) i/s   (84.06 ns/i) -     59.546M in   5.005745s

Comparison:
              master:  9406864.5 i/s
        sm/neon-vtbl: 11895905.7 i/s - 1.26x  faster

== 1k few matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl   277.531k i/100ms
Calculating -------------------------------------
        sm/neon-vtbl      2.722M (± 1.7%) i/s  (367.34 ns/i) -     13.877M in   5.098968s

Comparison:
              master:  1485804.8 i/s
        sm/neon-vtbl:  2722269.4 i/s - 1.83x  faster

== 1k many matches ==
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
        sm/neon-vtbl   178.565k i/100ms
Calculating -------------------------------------
        sm/neon-vtbl      1.787M (± 1.1%) i/s  (559.45 ns/i) -      9.107M in   5.095378s

Comparison:
              master:  1462148.5 i/s
        sm/neon-vtbl:  1787482.0 i/s - 1.22x  faster
```
@samyron

samyron commented Sep 10, 2026

Copy link
Copy Markdown
Author

I understand if you desire to keep the SIMD implementations the same between SSE2 and Neon. However, ARM provides table lookup instructions which are very convenient for this use case. Even better that every character that needs to be escaped is less than 64 so we can use a single vqtbl4q_u8.

This is the relevant assembly on master:

ldr q0, [x8]
cmeq v1.16b, v0.16b, v5.16b
cmeq v2.16b, v0.16b, v6.16b
cmeq v3.16b, v0.16b, v7.16b
cmeq v4.16b, v0.16b, v16.16b
cmeq v0.16b, v0.16b, v17.16b
orr v1.16b, v1.16b, v2.16b
orr v2.16b, v3.16b, v4.16b
orr v0.16b, v0.16b, v1.16b
orr v0.16b, v2.16b, v0.16b
shrn v0.8b, v0.8h, #0x4
fmov x11, d0
ands x11, x11, #0x8888888888888888
b.ne $+0x88
add x8, x8, #0x10
add x9, x9, #0x10

The generated assembly on this branch:

ldr q0, [x8]
tbl v0.16b, {v1.16b, v2.16b, v3.16b, v4.16b}, v0.16b
shrn v0.8b, v0.8h, #0x4
fmov x11, d0
ands x11, x11, #0x8888888888888888
b.ne $+0x7c
add x8, x8, #0x10
add x9, x9, #0x10

I also think the benchmark for 1k few matches is artificially inflated due to the bug fixed in this branch. The code on master generates an incorrect string due to the undefined behavior bug fixed in this branch.

irb(main):002> ERB::Util.html_escape((("a" * 127 + "<") * 8))
=> 
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;a
aaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&#39;aaaaaaaaaaaaaaa&lt;"

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