.NET 8 Search Optimization: SearchValues and FrozenCollections for Hot Paths
.NET 8 introduces SearchValues<T> and collections like FrozenSet<T>/FrozenDictionary<TKey, TValue>, which slash overhead in hot paths. These structures do the heavy lifting during creation, delivering constant-time lookups without repeated analysis. They're perfect for high-throughput apps handling millions of operations.
Boosting Character Search with SearchValues
The standard string.IndexOfAny re-analyzes the character set on every call, piling up overhead in loops. SearchValues<char> is created once and caches the optimal search strategy.
During initialization, .NET performs:
- Analysis of the input set (single char, range, or multi-char).
- Algorithm selection with SIMD support (AVX2, SSE2).
- Building data structures (bitmasks, hash tables).
SearchValues<char> separators = SearchValues.Create(".,;!?");
ReadOnlySpan<char> input = "Hello, world!";
int index = input.IndexOfAny(separators);
Search strategies vary by data:
- Single character — vectorized check.
- Range (0-9A-Z) — O(1) bitmask.
- Small set (≤4 chars) — SIMD.
- Large set — optimized hash table.
Benchmarks show massive gains:
| Scenario | IndexOfAny | SearchValues | Speedup |
|----------|------------|--------------|---------|
| Short string | ~5 ns | ~5 ns | 1x |
| Long string (2000 chars) | ~52 ns | ~4 ns | ~13x |
The biggest wins come with strings over 100 characters, skipping repeated analysis.
FrozenCollections: Immutable Hash Tables
HashSet<T> and Dictionary<TKey, TValue> carry mutation overhead like rehashing and bounds checks. FrozenSet<T> and FrozenDictionary<TKey, TValue> are tuned for read-only use cases.
During creation:
- Key analysis for a perfect hash function with zero collisions.
- Cache-friendly data layout.
- Stripped runtime immutability checks.
Small collections use bitmasks instead of hashing.
var dict = Enumerable.Range(0, 100000)
.ToFrozenDictionary(x => x, x => $"Value {{x}}");
if (dict.TryGetValue(42, out string? value))
{
Console.WriteLine(value);
}
Benchmarks (100,000 elements):
| Operation | Dictionary | HashSet | FrozenDictionary | FrozenSet |
|-----------|------------|---------|------------------|-----------|
| TryGetValue/Contains | 3.78 ns | 4.47 ns | 1.87 ns | 3.37 ns |
FrozenDictionary is twice as fast as a standard Dictionary.
Real-World Use Cases
SearchValues:
- Millions of lookups against fixed sets.
- Log parsing, tokenization, validation.
- Long strings (>100 characters).
FrozenCollections:
- Static configs loaded at startup.
- Lookup tables (error codes, statuses).
- Read-heavy workloads with thousands of lookups.
Skip them for frequent mutations or rare calls—the creation cost outweighs benefits.
Practical Implementation
Cache at the app level:
public static class AppCache
{
public static readonly SearchValues<char> UrlSafeChars =
SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~");
public static readonly FrozenSet<string> ValidCurrencies =
new[] { "USD", "EUR", "GBP", "JPY", "CNY" }.ToFrozenSet();
}
Validation examples:
- Email:
SearchValues<char> invalidEmailChars = SearchValues.Create("()<>[]:;@\\,?\""); - TLD:
FrozenSet<string> validTlds = LoadTldsFromConfig().ToFrozenSet();
Always profile with BenchmarkDotNet—payoff depends on operation volume.
Key Takeaways
SearchValuesspeeds up searches 13x on long strings via one-time optimization.FrozenDictionarydoublesTryGetValueperformance vs.Dictionary.- Both shine with immutable sets in hot paths.
- Profile first: useless for infrequent calls.
- Leverage SIMD and perfect hashing for mid/senior dev wins.
— Editorial Team
No comments yet.