I keep seeing SearchValues<T> described like a cheat code. Swap it in, string scanning gets five times faster, collect your applause. I happen to maintain a log parser that leans hard on IndexOfAny, so before refactoring anything I did what I always tell juniors to do and never do myself: I measured first.
Good thing too, because my first benchmark was a tie.
The setup
If you haven't met it, SearchValues<T> landed in .NET 8, lives in System.Buffers, and holds a precomputed, immutable set of values to search for. You build it once, and the runtime picks the fastest scanning strategy for that particular set.
static readonly SearchValues<char> Delimiters =
SearchValues.Create(['=', '&', ';', '?', '#']);
int i = span.IndexOfAny(Delimiters);
Enter fullscreen mode Exit fullscreen mode
My test: a ~32 MB fake log buffer, counting five delimiter characters, best of 5 runs after warmup, .NET 10, Release build, on a small Linux container. Not a lab. That's fine — I care about ratios, not absolute numbers.
IndexOfAny(char[]) : 10.2 ms matches: 265,398
manual char loop : 19.4 ms matches: 265,398
SearchValues<char> : 9.7 ms matches: 265,398
Enter fullscreen mode Exit fullscreen mode
So the fancy new type beat plain old string.IndexOfAny by half a millisecond. On 32 MB. The reason is that IndexOfAny with a small char set is already heavily vectorized on modern .NET — it got the same love SearchValues did.
Look at the middle line though. The hand-rolled foreach with a c is '=' or '&' or ';' check — the thing many of us write because it "avoids the API overhead" — is twice as slow as either of them. My take: the enemy here isn't IndexOfAny, it's our own clever loops.
Where it actually wins
My log buffer had a delimiter every ~60 characters. That's dense. The vectorized fast path barely gets going before it has to stop and report a match.
So I rebuilt the buffer with rare delimiters — one every ~40 KB, the "scanning for something unusual" case:
IndexOfAny(char[]) : 5.6 ms matches: 723
SearchValues<char> : 3.3 ms matches: 723
Enter fullscreen mode Exit fullscreen mode
Now it's about 1.7x faster. When matches are rare, SearchValues gets to do what it's built for: sprint through long stretches of nothing in wide vectorized steps.
One wrinkle I didn't expect: I assumed a bigger needle set (12 characters instead of 5) would widen the gap. On my machine it didn't — both landed around 10.6 ms. IndexOfAny clearly has tricks at that size too. I'm telling you this because benchmark posts that only show the flattering rows annoy me.
The trap that actually matters
Here's the number that should change how you write code. The whole point of SearchValues is paying the setup cost once. So what happens if you create it inside the method, per call? One million short lines, checked for delimiters:
static SearchValues : 25.1 ms
SearchValues per call : 70.2 ms
Enter fullscreen mode Exit fullscreen mode
Nearly three times slower — worse than never using the type at all. If SearchValues.Create isn't sitting in a static readonly field, you've built a slowdown machine with extra steps.
// yes
static readonly SearchValues<char> Delimiters = SearchValues.Create([...]);
// no. please no.
var delimiters = SearchValues.Create([...]); // inside a hot method
Enter fullscreen mode Exit fullscreen mode
So should you use it?
If your scan is a hot path over large buffers looking for rare characters: yes, and you'll feel it. If you're scanning short strings with dense matches through a small set, honestly, IndexOfAny with a cached char[] is already fine — don't file the refactoring ticket. And if you have manual character loops in hot code, replacing those is where the real 2x is hiding, whichever API you pick.
There's also SearchValues<string> for multi-substring search since .NET 9, which deserves its own post.
Everything here is runnable — one small console app, no dependencies. Numbers will differ on your hardware, which is exactly why the sample exists.
Full runnable sample: https://github.com/ssukhpinder/dev-to-code-samples/tree/main/001-searchvalues-fast-scanning
If this saved you a speculative refactor, or you've seen SearchValues win bigger somewhere, tell me in the comments — I'm collecting counterexamples.
— still benchmarking things nobody asked me to
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.