.NET 8 SearchValues 实战:别盲目追求性能提升
很多人把
性能直接掉了三倍,比不用它还慢。
下一篇
Voice AI 支付集成:用 Ringup 实现通话中快捷结账 →
SearchValues 当成性能作弊码,觉得只要换上它,字符串扫描速度就能翻几倍。但我维护的一个日志解析器大量使用了 IndexOfAny,在决定重构之前,我先做了实际的 Benchmark 测试,结果挺有意思:在某些场景下,它几乎没带来任何提升。SearchValues 在 .NET 8 中引入,本质上是一个预计算的不可变值集合。运行时会根据这个集合选择最快的扫描策略。
static readonly SearchValues Delimiters =
SearchValues.Create(['=', '&', ';', '?', '#']);
int i = span.IndexOfAny(Delimiters);我用了一个 32MB 的模拟日志缓冲区,在 .NET 10 的 Linux 容器环境下测试(Release 模式)。结果是:IndexOfAny(char[]) 用时 10.2ms,而 SearchValues 用时 9.7ms。
差距极小。原因是现代 .NET 对小字符集的 IndexOfAny 已经做了深度向量化优化,速度快得离谱。反而那些自认为“为了避免 API 开销”而手写的 foreach 循环,速度最慢(19.4ms),直接慢了一倍。所以,真正的性能杀手往往是我们自以为聪明的循环。
什么时候它才真正起作用?
当匹配项非常稀疏时,SearchValues 才会展现威力。我把缓冲区改为每 40KB 才出现一个分隔符,结果 SearchValues 比 IndexOfAny 快了 1.7 倍。因为在长距离扫描中,它能利用向量化指令大步跳跃。
一个必须注意的坑
SearchValues 的核心在于“一次创建,多次使用”。如果你在热点方法内部每次调用时都 Create 一次,性能会崩掉。
- 静态缓存(正确): 25.1 ms
- 每次调用创建(错误): 70.2 ms
性能直接掉了三倍,比不用它还慢。
// 正确做法:定义为静态只读字段
static readonly SearchValues Delimiters = SearchValues.Create(['=', '&', ';']);
// 错误做法:在热点方法内部创建
public void Process(ReadOnlySpan<char> span) {
var delimiters = SearchValues.Create(['=', '&', ';']);
// ...
}总结一下我的实操经验:如果你在处理大缓冲区且查找的字符很稀疏,果断用 SearchValues;如果你处理的是短字符串且匹配密集,原生的 IndexOfAny 足够了。但无论如何,请把手写的字符循环删掉,那才是真正的性能提升点。
完整的可运行示例在这里:
https://github.com/ssukhpinder/dev-to-code-samples/tree/main/dotnet/searchvalues