假设要设计一个方法,传入一个string字符串,判断其是否为Base64编码的字符串,常规写法是:
private static readonly char[] Base64CharsArr = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '+', '/'
};
public bool IsBase64(string text)
{
for (var i = 0; i < text.Length; i++)
{
var chr = text[i];
if (!Base64CharsArr.Contains(chr))
{
return false;
}
}
return true;
}
下面会介绍使用AsSpan和SearchValues进行优化。
注意:SearchValues仅支持.NET 8及以上,并且专注于支持char和byte类型
AsSpan
AsSpan 是一个用于高效处理内存数据的方法,它可以将数组、字符串或其他支持的内存区域转换为 Span<T> 或 ReadOnlySpan<T> 类型。Span<T> 是 .NET 中引入的一种高性能、低开销的数据结构,用于表示连续内存区域的视图,而无需分配额外的内存或复制数据。
简要总结:
**Span<T>**:表示一段连续内存的视图,支持读写操作。**ReadOnlySpan<T>**:表示一段只读的连续内存视图。- 零开销:
AsSpan不会复制数据,而是直接引用原始内存区域,因此性能非常高。 - 适用范围:适用于数组、字符串、栈内存等连续内存区域。
其主要用于:
- 高效处理数组和字符串:避免创建子字符串或子数组的副本。
- 与高性能API结合:与
Memory<T>、ReadOnlyMemory<T>和Span<T>相关的API结合使用,提升性能。 - 减少内存分配:通过避免不必要的内存分配,降低GC压力。
下面使用AsSpan实现一开始的方法:
private static readonly char[] Base64CharsArr = {'A', 'B','………………'};
public bool IsBase64(string text)
{
//IndexOfAnyExcept 是一个用于高效搜索字符或字节的方法,它返回当前 Span<T> 或 ReadOnlySpan<T> 中第一个不包含在指定集合中的字符或字节的索引,所以字节都在集合中则返回-1
return text.AsSpan().IndexOfAnyExcept(Base64CharsArr) == -1;
}
SearchValues
SearchValues<T> 是.NET 8引入的一个高性能工具类,旨在优化对一组特定值的搜索操作。它通过预先处理搜索模式,减少重复计算开销,从而提升后续搜索效率。
概况:
- 用途:高效搜索一组值(如字符或字节),适用于需要频繁搜索的场景(如字符串解析、二进制数据处理)。
- 支持类型:主要针对
char和byte,因其常用于文本和二进制操作。 - 性能优化:通过预处理(如构建查找表或位掩码)加速搜索,减少运行时开销。
特性:
- 预先处理:将待搜索的值集合预处理为优化结构,供后续重复使用。
- 与Span结合:与
Span<T>或ReadOnlySpan<T>配合使用,利用高性能API(如IndexOfAny)。 - 线程安全:实例是只读的,可在多线程中安全共享。
适用于:
- 高频搜索:如解析器、词法分析器需多次搜索相同字符集。
- 高性能需求:处理大文本或二进制数据时减少CPU开销。
- 替代
**IndexOfAny**:当需要多次调用IndexOfAny时,使用SearchValues可避免重复处理参数。
下面进一步对开始的方法优化:
private const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
//此处Base64Chars和Base64CharsArr选择没有区别,他们都会被隐式转换为SpanReadOnly<char>
//但是使用Base64Chars可以在后续进行进一步优化(尽管速度已经快到极致了)
private static readonly SearchValues<char> Base64SearchVal = SearchValues.Create(Base64Chars);
public bool IsBase64_SearchValues(string text)
{
return text.AsSpan().IndexOfAnyExcept(Base64SearchVal) == -1;
}
方法性能对比
用三种不同的搜索方法实现判断一个字符串是否为Base64编码的逻辑,然后用Benchmark进行基准测试。
using System.Buffers;
using BenchmarkDotNet.Attributes;
namespace ConsoleApp1;
public class Benchmarks
{
private const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static readonly char[] Base64CharsArr = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '+', '/'
};
//此处Base64Chars和Base64CharsArr选择没有区别,他们都会被隐式转换为SpanReadOnly<char>
//但是使用Base64Chars可以在后续进行进一步优化(尽管速度已经快到极致了)
private static readonly SearchValues<char> Base64SearchVal = SearchValues.Create(Base64Chars);
[Params("afdsafdsafdsac","tg53gbhy46hb^Eww","oidngojsogj903295090$@090joisejf-03()jio34gbfdbhgnj,.;/ipolkjegtrf3qfrw4")]
public string ExampleText { get; set; }
[Benchmark]
public bool IsBase64_SearchValues()
{
return ExampleText.AsSpan().IndexOfAnyExcept(Base64SearchVal) == -1;
}
[Benchmark]
public bool IsBase64_CharArray()
{
return ExampleText.AsSpan().IndexOfAnyExcept(Base64CharsArr) == -1;
}
[Benchmark]
public bool IsBase64_Naive()
{
for (var i = 0; i < ExampleText.Length; i++)
{
var chr = ExampleText[i];
if (!Base64CharsArr.Contains(chr))
{
return false;
}
}
return true;
}
}
其测试结果如下:
随着测试数据长度的增加,三种方法的速度差距快速拉大,在测试长度为83的字符串时,SearchValues方法处理速度是Naive方法的130倍。