在 .NET 10 中,当扫描大型缓冲区以查找稀疏分隔符时,SearchValues<T> 终于展现出了可衡量的优势:在处理 32 MB 的负载时,SearchValues 仅需 3.3 ms,而 IndexOfAny 则需要 5.6 ms。
为什么会出现 SearchValues
.NET 8 引入了 SearchValues<T>,用于预计算一组字符(或字节),并让运行时为该集合选择最快的扫描算法。只需构建一次对象,然后在需要于 span 中定位任何值时重复使用它即可。运行时可能会采用向量化指令、无分支循环或其他底层技巧,如果不付出巨大努力,手写的替代方案很难与之媲美。
至关重要的基准测试
引发讨论的测试使用了包含五个分隔符字符的 32 MB 缓冲区。在 .NET 10 上对三种方法进行了计时:
- IndexOfAny(char[]) – 10.2 ms
- Manual foreach loop over the span – 19.4 ms
- SearchValues
– 9.7 ms
SearchValues 仅比内置的 IndexOfAny 快了半毫秒。这个结果并不是论坛上流传的那种戏剧性的“快五倍”的说法;现代 .NET 已经针对小集合优化了 IndexOfAny,从而缩小了差距。
当分隔符很稀疏时——每 40 KB 才出现一次,而不是每 60 字节出现一次——情况就不同了:
- IndexOfAny(char[]) – 5.6 ms
- SearchValues
– 3.3 ms
现在 SearchValues 快了 1.7 倍,因为引擎通过向量化步骤快速跳过长段的不匹配数据,而 IndexOfAny 则回退到了一种不够激进的路径。
隐藏的成本
SearchValues 并非免费。构建对象会分配内存并构建内部查找表。如果你在每次方法调用时都实例化它,那么开销将使任何运行时的收益都显得微不足道。在一个对短行进行一百万次扫描例程调用的基准测试中显示:
- Static (reused) SearchValues – 总计 25.1 ms
- Per-call SearchValues – 总计 70.2 ms
每次调用都创建对象会使整个操作比完全不使用 SearchValues 慢三倍。请将实例存储在 static readonly 字段中,或者在多次调用之间重复使用它。
何时使用 SearchValues
- 大输入,少匹配 – 当扫描器可以跳过长段不含分隔符的数据时,向量化路径的优势就会显现。
- 使用相同集合的重复扫描 – 如果需要多次搜索相同的字符,一次性的设置成本是值得的。
何时继续使用 IndexOfAny
- 短字符串或多匹配 – 额外的设置成本超过了边际速度收益。
- 代码已经在使用 IndexOfAny – 现代 .NET 的实现已经针对小字符集进行了高度优化,因此更换为
SearchValues可能不会带来显著变化。
常见陷阱
- 在热点循环中嵌入构造过程 – 这会导致上述三倍的减速。
- 试图用手动循环来“智取”运行时 – 手动
foreach版本比两种内置方法都慢一倍,这证实了如果没有深入了解 SIMD 指令,很难超越 .NET 库。 - 假设性能会普遍提升 – 在密集数据上仅快半毫秒的成绩证明了
SearchValues并不是万能的“作弊码”;其优势取决于数据。
总结
只有当你扫描大型缓冲区以查找不频繁出现的字符,并且能够承担一次性构建成本时,SearchValues<T> 才会带来明显的优势。对于大多数日常字符串搜索场景——短输入、频繁匹配或代码已经依赖 IndexOfAny——内置方法仍然是更简单、更快的选择。有选择性地使用 SearchValues,缓存实例,你就能在收获实际性能提升的同时,避免隐藏的性能惩罚。
源代码和完整测试套件:https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l
