C#极致优化字符查询,性能轻松提升百倍

292 阅读4分钟

假设要设计一个方法,传入一个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 不会复制数据,而是直接引用原始内存区域,因此性能非常高。
  • 适用范围:适用于数组、字符串、栈内存等连续内存区域。

其主要用于:

  1. 高效处理数组和字符串:避免创建子字符串或子数组的副本。
  2. 与高性能API结合:与 Memory<T>ReadOnlyMemory<T>Span<T> 相关的API结合使用,提升性能。
  3. 减少内存分配:通过避免不必要的内存分配,降低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引入的一个高性能工具类,旨在优化对一组特定值的搜索操作。它通过预先处理搜索模式,减少重复计算开销,从而提升后续搜索效率。

概况:

  • 用途:高效搜索一组值(如字符或字节),适用于需要频繁搜索的场景(如字符串解析、二进制数据处理)。
  • 支持类型:主要针对charbyte,因其常用于文本和二进制操作。
  • 性能优化:通过预处理(如构建查找表或位掩码)加速搜索,减少运行时开销。

特性:

  1. 预先处理:将待搜索的值集合预处理为优化结构,供后续重复使用。
  2. 与Span结合:与Span<T>ReadOnlySpan<T>配合使用,利用高性能API(如IndexOfAny)。
  3. 线程安全:实例是只读的,可在多线程中安全共享。

适用于:

  • 高频搜索:如解析器、词法分析器需多次搜索相同字符集。
  • 高性能需求:处理大文本或二进制数据时减少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倍。