Net优化之字符串性能优化

122 阅读3分钟

本文章探讨如何在操作字符串时保证高运行速度的同时最大程度的优化内存使用(内存占用降低至0)

现在有这样一个场景:

需要提供一个方法,接收传入的url链接(字符串)返回链接中的主机地址(Host)

快速实现这个需求:

string GetHost(string url)
{
    var uri = new Uri(url);
    return uri.Host;
}

var Url = "https://juejin.cn/post/7477390855746797580";

Console.WriteLine(GetHost(Url));
//输出:juejin.cn

这样实现起来相当的简单,但是在使用Uri类时系统会生成一些无用的字符串占用内存资源,并且这个方法运行速度上不够理想。

因此我们可以使用其他方法,如字符串拼接,通过indexOf和substring来实现:

public string GetHost(string Url)
{
    var prefix = Url.IndexOf("://", StringComparison.Ordinal);
    var start = prefix == -1 ? 0 : prefix + 3;
    var end = Url[start..].IndexOf('/');

    return end == -1 ? Url[start..] : Url.Substring(start, end);
}

该方法只操作必要的字符串,处理速度有了很大的提升,不过当Url字符串长度很长的时候可能会占用大量的内存资源。因此,可以使用AsSpan对其进一步优化:

public string GetHost(string Url)
{
    var prefix = Url.AsSpan().IndexOf([':', '/', '/']);
    var start = prefix == -1 ? 0 : prefix + 3;
    var end = Url.AsSpan(start).IndexOf('/');

    var span = end == -1 ? Url.AsSpan(start) : Url.AsSpan(start, end);

    return span.ToString();
}

使用AsSpan将start或end转为ReadOnlySpan类型,不占用额外的内存。这个方法距离“占用内存为0”的目标很接近了。要实现这个目标,可以用CommunityToolkit.HighPerformance包,它是微软官方维护的高性能开发工具包,属于 .NET 基金会下的开源项目,专为优化 .NET 应用性能而设计。

使用社区性能包CommunityToolkit.HighPerformance将字符变量缓存,在高并发场景中实现内存0分配的同时又具备高性能。

public string GetHost(string Url)
{
    var prefix = Url.AsSpan().IndexOf([':', '/', '/']);
    var start = prefix == -1 ? 0 : prefix + 3;
    var end = Url.AsSpan(start).IndexOf('/');

    var span = end == -1 ? Url.AsSpan(start) : Url.AsSpan(start, end);

    return StringPool.Shared.GetOrAdd(span);
}

下面进行基准测试,看看这四个方法具体表现如何:

注:使用的参数长度分别为45,137,357

[MemoryDiagnoser]
public class StringBenchmark
{
    [Params("https://juejin.cn/post/7477390855746797580", "https://juejin.cn/post/7477390855746797580/togjeibmetmgekrgiowf.....", "https://juejin.cn/post/7477390855746797580/togjeibmetmgekrgiowfjoiewnvgroeirtjbonbeiogmsof........")]
    public string Url { get; set; }
    [Benchmark]
    public string GetHost_V1()
    {
        var uri = new Uri(Url);
        return uri.Host;
    }
    [Benchmark]
    public string GetHost_V2()
    {
        var prefix = Url.IndexOf("://", StringComparison.Ordinal);
        var start = prefix == -1 ? 0 : prefix + 3;
        var end = Url[start..].IndexOf('/');

        return end == -1 ? Url[start..] : Url.Substring(start, end);
    }
    [Benchmark]
    public string GetHost_V3()
    {
        var prefix = Url.AsSpan().IndexOf([':', '/', '/']);
        var start = prefix == -1 ? 0 : prefix + 3;
        var end = Url.AsSpan(start).IndexOf('/');

        var span = end == -1 ? Url.AsSpan(start) : Url.AsSpan(start, end);

        return span.ToString();
    }
    [Benchmark]
    public string GetHost_V4()
    {
        var prefix = Url.AsSpan().IndexOf([':', '/', '/']);
        var start = prefix == -1 ? 0 : prefix + 3;
        var end = Url.AsSpan(start).IndexOf('/');

        var span = end == -1 ? Url.AsSpan(start) : Url.AsSpan(start, end);

        return StringPool.Shared.GetOrAdd(span);
    }
}

测试结果:

从上可以看出,V2版本的方法在字符串长度较低时,其速度远大于V1方法,内存占用也小。但随着字符长度的增加,V2方法使用的内存也成倍增加,而V1方法使用内存在达到一个高点后不再增加。

作为V2的改进方法V3又进一步优化了性能,同时由于AsSpan的特性只占有少量的内存,并且内存占用是恒定的。

V4方法在V3的基础上使用了字符缓存池,内存占用为0,代价是牺牲了一些性能(StringPool是为内存而设计的)。

综上,可以根据场景灵活的选择使用V3或者V4。