优秀的编程知识分享平台

网站首页 > 技术文章 正文

使用Span提高C#代码的性能(c#中哪些准则可以提高代码质量)

nanyue 2024-07-22 13:52:28 技术文章 7 ℃

根据我的经验,提高应用程序性能的主要方法是减少IO调用的次数和持续时间。然而,一旦执行了这个选项,开发人员采取的另一种方式就是在堆栈上使用内存。堆栈允许非常快的分配和释放,尽管它应该只用于分配小的部分,因为堆栈大小是非常小的。另外,使用堆栈可以减少GC的压力。为了在堆栈上分配内存,将值类型或stackalloc操作符与非托管内存的使用结合使用。

第二个选项很少被开发人员使用,因为用于非托管内存访问的API非常冗长。

Span<T>是C# 7.2中到达的一组值类型,它是来自不同来源的内存的无分配表示。Span<T>允许开发人员以更方便的方式处理连续内存区域,确保内存和类型安全。

Span实现

Ref返回

对于那些不密切关注C#语言更新的人来说,了解Span<T>实现的第一步是了解C# 7.0中引入的ref返回值。

虽然大多数读者都熟悉通过引用传递方法参数,但现在C#允许返回对值的引用,而不是值本身。

让我们来看看它是如何工作的。我们将为一组杰出的音乐家创建一个简单的包装,它既展示了传统的行为,又展示了新的ref返回特性。

public class ArtistsStore  
{  
    private readonly string[] _artists = new[] { "Amenra", "The Shadow Ring", "Hiroshi Yoshimura" };  
  
    public string ReturnSingleArtist()  
    {  
        return _artists[1];  
    }  
  
    public ref string ReturnSingleArtistByRef()  
    {  
        return ref _artists[1];  
    }  
  
    public string AllAritsts => string.Join(", ", _artists);  
}  

现在我们调用这些方法。

var store = new ArtistsStore();  
var artist = store.ReturnSingleArtist();  
artist = "Henry Cow";  
var allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura  
  
artist = store.ReturnSingleArtistByRef();  
artist = "Frank Zappa";  
allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura  
  
ref var artistReference = ref store.ReturnSingleArtistByRef();  
artistReference = "Valentyn Sylvestrov";  
allArtists = store.AllAritsts; //Amenra, Valentyn Sylvestrov, Hiroshi Yoshimura  

注意,在第一个和第二个示例中,原始集合没有被修改。在最后一个例子中,我们成功地改变了这个集合的第二位艺术家。在本文后面的过程中您将看到,这个有用的特性将帮助我们以类似引用的方式操作位于堆栈上的数组。

Ref结构

我们知道,值类型可以在堆栈上分配。而且,它们并不一定依赖于使用值的上下文。为了确保值总是分配在堆栈上,C# 7.0中引入了ref struct的概念。Span<t>是一个ref struct,所以我们确定它总是分配在堆栈上。

Span实现

Span<T>是一个引用结构,它包含一个指向内存的指针和类似于以下内容的跨度长度。

public readonly ref struct Span<T>  
{  
  private readonly ref T _pointer;  
  private readonly int _length;  
  public ref T this[int index] => ref _pointer + index;  
  ...  
}  

注意指针字段附近的ref修饰符无法在.NET Core中的普通C#中声明此类构造,而是通过ByReference <T>实现。

因此,如您所见,索引是通过ref return实现的,它允许仅堆栈结构的引用类型类似于行为。

span限制

为了确保引用结构始终在堆栈上使用,它具有许多限制; 即,它们不能被装箱,它们不能被分配给对象类型,动态类型或任何接口类型的变量,它们不能是引用类型中的字段,并且它们不能在await和yield中使用 边界。 另外,对两个方法Equals和GetHashCode的调用将引发NotSupportedException。 Span<T>是一个引用结构。

使用Span代替字符串

重做现有代码库。

让我们研究一下将Linux权限转换为八进制表示形式的代码。 您可以在这里访问它。 这是原始代码。

internal class SymbolicPermission  
{  
    private struct PermissionInfo  
    {  
        public int Value { get; set; }  
        public char Symbol { get; set; }  
    }  
  
    private const int BlockCount = 3;  
    private const int BlockLength = 3;  
    private const int MissingPermissionSymbol = '-';  
  
    private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {  
            {0, new PermissionInfo {  
                Symbol = 'r',  
                Value = 4  
            } },  
            {1, new PermissionInfo {  
                Symbol = 'w',  
                Value = 2  
            }},  
            {2, new PermissionInfo {  
                Symbol = 'x',  
                Value = 1  
            }} };  
  
    private string _value;  
  
    private SymbolicPermission(string value)  
    {  
        _value = value;  
    }  
  
    public static SymbolicPermission Parse(string input)  
    {  
        if (input.Length != BlockCount * BlockLength)  
        {  
            throw new ArgumentException("input should be a string 3 blocks of 3 characters each");  
        }  
        for (var i = 0; i < input.Length; i++)  
        {  
            TestCharForValidity(input, i);  
        }  
  
        return new SymbolicPermission(input);  
    }  
  
    public int GetOctalRepresentation()  
    {  
        var res = 0;  
        for (var i = 0; i < BlockCount; i++)  
        {  
            var block = GetBlock(i);  
            res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);  
        }  
        return res;  
    }  
  
    private static void TestCharForValidity(string input, int position)  
    {  
        var index = position % BlockLength;  
        var expectedPermission = Permissions[index];  
        var symbolToTest = input[position];  
        if (symbolToTest != expectedPermission.Symbol && symbolToTest != MissingPermissionSymbol)  
        {  
            throw new ArgumentException(#34;invalid input in position {position}");  
        }  
    }  
  
    private string GetBlock(int blockNumber)  
    {  
        return _value.Substring(blockNumber * BlockLength, BlockLength);  
    }  
  
    private int ConvertBlockToOctal(string block)  
    {  
        var res = 0;  
        foreach (var (index, permission) in Permissions)  
        {  
            var actualValue = block[index];  
            if (actualValue == permission.Symbol)  
            {  
                res += permission.Value;  
            }  
        }  
        return res;  
    }  
}  
  
public static class SymbolicUtils  
{  
    public static int SymbolicToOctal(string input)  
    {  
        var permission = SymbolicPermission.Parse(input);  
        return permission.GetOctalRepresentation();  
    }  
}  

推理非常简单:string是char的数组,所以为什么不将其分配在堆栈上而不是堆上。

因此,我们的首要目标是将SymbolicPermission的字段_value标记为ReadOnlySpan <char>而不是字符串。 为此,我们必须将SymbolicPermission声明为引用结构,因为字段或属性的类型不能为Span <T>,除非它是引用结构的实例。

internal ref struct SymbolicPermission  
{  
    ...  
    private ReadOnlySpan<char> _value;  
}  

现在,我们只需将触及范围内的每个字符串更改为ReadOnlySpan <char>,唯一感兴趣的一点是GetBlock方法,因为在这里,我们将Substring替换为Slice。

private ReadOnlySpan<char> GetBlock(int blockNumber)  
{  
    return _value.Slice(blockNumber * BlockLength, BlockLength);  
} 

评价

我们来衡量结果。

我们注意到速度提高了50纳秒,大约提高了10%的性能。有人可能会说,50纳秒并不多,但它几乎没有花费我们来实现它!

现在,我们来评估一下使用18块12个字符的权限的改进,看看我们是否能获得显著的改进。

如您所见,我们获得了0.5微秒或5%的性能改进。再一次,这看起来可能是一个适度的成就。但是记住,这是很容易实现的。

使用span而不是数组

让我们扩展其他类型的数组。 考虑一下ASP.NET Channels管道中的示例。 下面代码背后的原因是,数据经常通过网络以块的形式到达,这意味着该数据段可能同时驻留在多个缓冲区中。 在示例中,此类数据被解析为int。

public unsafe static uint GetUInt32(this ReadableBuffer buffer) {  
    ReadOnlySpan<byte> textSpan;  
  
    if (buffer.IsSingleSpan) { // if data in single buffer, it’s easy  
        textSpan = buffer.First.Span;  
    }  
    else if (buffer.Length < 128) { // else, consider temp buffer on stack  
        var data = stackalloc byte[128];  
        var destination = new Span<byte>(data, 128);  
        buffer.CopyTo(destination);  
        textSpan = destination.Slice(0, buffer.Length);  
    }  
    else {  
        // else pay the cost of allocating an array  
        textSpan = new ReadOnlySpan<byte>(buffer.ToArray());  
    }  
  
    uint value;  
    // yet the actual parsing routine is always the same and simple  
    if (!Utf8Parser.TryParse(textSpan, out value)) {  
        throw new InvalidOperationException();  
    }  
    return value;  
} 

让我们来分析一下这里发生了什么。我们的目标是将字节序列解析为uint。

if (!Utf8Parser.TryParse(textSpan, out value)) {  
    throw new InvalidOperationException();  
}  
return value;  

现在让我们看看如何将输入参数填充到textSpan中。 输入参数是一个缓冲区的实例,可以读取一系列连续的字节。

ReadableBuffer继承自ISequence <ReadOnlyMemory <byte >>,这基本上意味着它由多个内存段组成。

如果缓冲区由单个段组成,我们只使用第一个段的基础Span。

if (buffer.IsSingleSpan) {  
    textSpan = buffer.First.Span;  
}  

否则,我们在堆栈上分配数据并基于它创建Span <byte>。

var data = stackalloc byte[128];  
var destination = new Span<byte>(data, 128);  

然后,我们使用方法buffer.CopyTo(destination)遍历缓冲区的每个内存段,并将其复制到目标Span。 之后,我们只对缓冲区长度的跨度进行切片。

此示例向我们展示了新的Span <T> API,使我们能够以比到达之前更方便的方式使用在堆栈上手动分配的内存。

结论

Span <T>为stackalloc提供了一种安全且易于使用的替代方法,可轻松提高性能。 Span <T>在.NET Core 3.0代码库中得到了广泛的使用,与以前的版本相比,它使我们在性能上得到了改善。

在决定是否应使用Span <T>时,您可能会考虑以下几点:

  • 如果您的方法接受数据数组且未更改其大小。 如果您不修改输入,则可以考虑ReadOnlySpan <T>。
  • 如果您的方法接受用于计数某些统计信息或执行语法分析的字符串,则应接受ReadOnlySpan <char>。
  • 如果您的方法返回一小段数据,则可以在Span <T>的帮助下返回Span <T> buf = stackalloc T [size]。 请记住,T应该是一个值类型。

Tags:

最近发表
标签列表