roc-regex does what you'd expect. It's very fast.
| pattern | Roc ns | Rust ns | Roc / Rust | what it exercises |
|---|---|---|---|---|
Holmes |
28350 | 27027 | 1.05x | literal search, dense hits |
Moriarty |
10250 | 9969 | 1.03x | literal search, rare hits |
Sherlock|Holmes|Watson|… (8 names) |
479750 | 425248 | 1.13x | Teddy multi-literal scan |
[A-Za-z]+ |
1613400 | 2205824 | 0.73x | one class repeated: no reverse automaton |
[0-9]{2,4} |
64250 | 108872 | 0.59x | the same, over a sparse class |
\bthe\b |
209300 | 204994 | 1.02x | prefix search plus word boundaries |
\w+\s+\w+ |
2055750 | 1544425 | 1.33x | bare automaton, no accelerator |
(\w+)@(\w+) |
67350 | 68611 | 0.98x | prefix search on a rare byte |
\p{L}+ |
2007550 | 2061462 | 0.97x | Unicode class, non-ASCII decoding |
.*Holmes |
313550 | 644285 | 0.49x | prefix search plus newline skipping |
It pre-compiles the patterns at build time as a side product of Roc's constant folding, which I think is neat. I also tried to get it to beat the fastest HTTP parser+route matcher combo I know about, which was useful for finding startup overhead, but it's about 5 times slower. There just aren't enough bytes in a HTTP request for the higher speed matching to beat out the startup overhead for a highly tuned domain.
As a warning, these numbers are using my patched local Roc compiler so getting full performance for the scan heavy benchmarks will require this issue to be solved. These are also being run on a machine with 128bit SIMD vectors and Rust can take advantage of 256bit vectors while this cannot.
There's a comparatively hefty amount of overhead in these numbers from using checked array access so if those get elided it'll go faster. The two-word match at 1.33x and the multi-literal scan at 1.13x are the most direct apples to apples Roc vs Rust comparisons.
woooooow, super impressive!!!
I landed a fix for that issue btw - lmk if you can still get this perf on the new main!
this is really exciting stuff, @Karl! :smiley:
Things mostly hold up but I'm seeing a ~25% regression in the sparse literal row (Moriarty) which is the most directly affected by SIMD. If things were just not working then I'd see a regression across the other scanner rows but those numbers are stable. Looking into it.
Only shows up in the full benchmark binary, not in isolation so the fix you landed is working fine. I'll be reporting bugs related to the regex code over the next few days as I get them isolated with repros.
Claude says: "It is loop alignment. The code did not change at all. This is not a SIMD regression and not a codegen-quality regression. It is the alignment sibling of the register-pressure effect your notes already describe, where an unrelated edit moves a row that never runs the changed code."
"There is a genuine upstream item in this. LLVM can align loop headers, and Roc is evidently not asking it to."
I'll do another pass at slimming down the core of the matcher regardless but that's the current state of things.
I have a hobby project writing a regular expression language in Roc, so this will be usefull reference material :eyes:. Great work!
https://github.com/ieviev/resharp-dotnet
Have you heard about this? It was faster than rust in many cases (dotnet simd got really good in dotnet 10)
https://iev.ee/blog/resharp-how-we-built-the-fastest-regex-in-fsharp/ very nice article about the details and what it does well
The part about (a|ab)+ vs (ab|a)+ in a string like aababaabab is very interesting
This uses their algorithm.
Awesome!
I also have a noob question - why is roc not able to use the bigger simd vectors? Is it just for now or is it some fundamental issue?
Library support. Roc only implements the 128 bit SIMD set which has broad hardware support while 256 bit is more limited and Richard is worried about long term support/complexity.
I believe the 256 bit worry was mainly about different behavior on different devices.
that was a different thing
there are certain instructions we just have to be careful not to use (they're really niche though, fortunately) because they give different answers for the same inputs depending on whether you're using aarch64 vs x86-64 vs wasm
the 256-bit thing is basically:
so for now, 128-bit it is :smile:
Karl said:
I'll do another pass at slimming down the core of the matcher regardless but that's the current state of things.
I cleaned up the code today (the original quality was...not good, this is readable to me) and added another optimization for leading x+ patterns which is a bit golfing for my benchmark set but I think is reasonably general.
Last updated: Sep 24 2026 at 15:59 UTC