When SearchValues Actually Pays Off
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);
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.
| Method | Time | Matches |
|---|---|---|
IndexOfAny(char[]) |
10.2 ms | 265,398 |
| manual char loop | 19.4 ms | 265,398 |
SearchValues<char> |
9.7 ms | 265,398 |
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:
| Method | Time | Matches |
|---|---|---|
IndexOfAny(char[]) |
5.6 ms | 723 |
SearchValues<char> |
3.3 ms | 723 |
Now it's about 1.7ร 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:
| Pattern | Time |
|---|---|
static SearchValues |
25.1 ms |
SearchValues per call |
70.2 ms |
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
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 2ร 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
Comments
No comments yet. Start the discussion.