使用跨度 提高C#代码的性能

介绍 (Introduction)

In my experience, the main thing to do in order to improve application performance is to reduce the number and duration of IO-calls. However, once this option is exercised, another path that developers take is using memory on stack. Stack allows very fast allocation and deallocation although it should be used only for allocating small portions since stack size is pretty small. Also, using stack allows reducing pressure on GC. In order to allocate memory on stack, one uses value types or stackalloc operator combined with the usage of unmanaged memory.

以我的经验,要提高应用程序性能,主要要做的是减少IO调用的次数和持续时间。 但是,一旦执行了该选项,开发人员所采取的另一条路径就是使用堆栈上的内存。 堆栈允许非常快速的分配和重新分配,尽管由于堆栈大小很小,堆栈仅应用于分配小部分。 同样,使用烟囱可以减少GC的压力。 为了在堆栈上分配内存,一种方法是使用值类型stackalloc运算符,并结合使用非托管内存。

The second option is rarely used by developers since API for unmanaged memory access is quite verbose.

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

Span<T> is a family of value types that arrived in C# 7.2 which is an allocation-free representation of memory from different sources. Span<T> allows developers to work with regions of contiguous memory in a more convenient fashion ensuring memory and type safety.

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

Span <t>实现 (Span<t> Implementation)

引用返回 (Ref return)

The first step in wrapping head around Span<T> implementation for those who don’t closely follow updates in C# language is learning about ref returns which were introduced in C# 7.0.

对于不密切关注C#语言更新的用户,将Span<T>实现包装起来的第一步是了解C#7.0中引入的ref返回

While most of the readers are familiar with passing method argument by reference, now C# allows returning a reference to a value instead of the value itself.

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

Let us examine how it works. We’ll create a simple wrapper around an array of prominent musicians which exhibits both traditional behavior and new ref return feature.

让我们检查一下它是如何工作的。 我们将围绕着一系列杰出的音乐家创建一个简单的包装,既展示传统行为又展示新的参考返回功能。

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);
}

Now let’s call those methods:

现在让我们调用这些方法:

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

Observe that while in the first and the second example, the original collection is unmodified, in the final example, we’ve managed to alter the second artist of the collection. As you’ll see later during the course of the article, this useful feature will help us operate arrays located on the stack in a reference-like fashion.

请注意,在第一个和第二个示例中,原始收藏集未更改,而在最后一个示例中,我们设法更改了该收藏集的第二个艺术家。 如您在本文后面所看到的那样,此有用的功能将帮助我们以类似引用的方式操作位于堆栈上的数组。

引用结构 (Ref structs)

As we know, value types might be allocated on stack. Also, they do not necessarily depend on the context where the value is used. In order to make sure that the value is always allocated on stack, the concept of ref struct was introduced in C# 7.0. Span<T> is a ref struct so we are sure that is always allocated on stack.

众所周知,值类型可能在堆栈上分配。 同样,它们不一定取决于使用该值的上下文。 为了确保始终在堆栈上分配该值,在C#7.0中引入了ref struct的概念。 Span<T>是一个ref struct因此我们确定它总是在堆栈上分配。

Span <t>实现 (Span<t> Implementation)

Span<T> is a ref struct which contains a pointer to memory and length of the span similar to below.

Span<T>是一个ref struct ,它包含一个指向内存的指针以及类似于以下内容的span长度。

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

Note ref modifier near the pointer field. Such construct can’t be declared in a plain C# in .NET Core it is implemented via ByReference<T>.

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

So as you can see, indexing is implemented via ref return which allows reference-type-like behavior for stack-only struct.

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

Span <t>限制 (Span<t> Limitations)

To ensure that the ref struct is always used on stack, it possesses a number of limitations, i.e., including that they can’t be boxed, they can’t be assigned to variables of type object, dynamic or to any interface type, they can’t be fields in a reference type, and they can’t be used across await and yield boundaries. In addition, calls to two methods, Equals and GetHashCode, throw a NotSupportedException. Span<T> is a ref struct.

为了确保ref struct始终在堆栈上使用,它具有许多限制,例如,不能将它们装箱,不能将它们分配给object类型, dynamic类型或任何接口类型的变量,不能是引用类型中的字段,也不能跨awaityield边界使用它们。 另外,对两个方法EqualsGetHashCode调用将引发NotSupportedExceptionSpan<T>是一个ref struct

使用Span <T>代替字符串 (Using Span<T> instead of string)

重做现有代码库以使用Span <t> (Reworking Existing Codebase to Use Span<t>)

Let’s examine code that converts Linux permissions to octal representation. You can access it here. Here is the original code:

让我们研究一下将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($"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();
    }
}

The reasoning is pretty straightforward: string is an array of char, so why not allocate it on stack instead of heap.

原因非常简单: stringchar的数组,所以为什么不将其分配在堆栈上而不是堆上。

So our first goal is to mark field _value of SymbolicPermission as ReadOnlySpan<char> instead of string. To achieve this, we must declare SymbolicPermission as ref struct since field or property cannot be of type Span<T> unless it’s an instance of a ref struct.

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

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

Now we just change every string within our reach to ReadOnlySpan<char>. The only point of interest is GetBlock method since here we replace Substring with Slice.

现在,我们将范围内的每个string都更改为ReadOnlySpan<char> 。 唯一感兴趣的是GetBlock方法,因为这里我们用Slice代替Substring

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

评价 (Evaluation)

Let’s measure the outcome:

让我们来衡量结果:

Image 1

We notice the speed up which accounts for 50 nanoseconds which is about 10% of performance improvement. One can argue that 50 nanoseconds are not that much but it cost almost nothing for us to achieve it!

我们注意到速度提高了50纳秒,大约是性能提高的10%。 可以说50纳秒不算什么,但是实现它几乎不花钱!

Now we’re going to evaluate this improvement on permission having 18 blocks of 12 characters each to see whether we can gain significant improvements.

现在,我们将评估具有18个块(每个字符12个字符)的权限上的改进,以查看我们是否可以获得重大改进。

Image 2

As you can see, we’ve managed to gain 0.5 microsecond or 5% performance improvement. Again, it may look like a modest achievement. But remember that this was really low hanging fruit.

如您所见,我们设法获得了0.5微秒或5%的性能提升。 同样,它看起来像是一个微不足道的成就。 但是请记住,这确实是低落的果实。

使用Span <T>代替数组 (Using Span<T> Instead of Arrays)

Let's expand on arrays of other types. Consider the example from ASP.NET Channels pipeline. The reasoning behind the code below is that data often arrives in chunks over the network which means that the piece of data may reside in multiple buffers simultaneously. In the example, such data is parsed to int.

让我们扩展其他类型的数组。 考虑一下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;
}

Let’s break it down a bit about what happens here. Our goal is to parse the sequence of bytes textSpan into uint.

让我们分解一下这里发生的事情。 我们的目标是将textSpan字节序列解析为uint

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

Now let’s have a look at how we populate our input parameter into textSpan. The input parameter is an instance of a buffer that can read a sequential series of bytes. ReadableBuffer is inherited from ISequence<ReadOnlyMemory<byte>> which basically means that it consists of multiple memory segments.

现在让我们看看如何将输入参数填充到textSpan 。 输入参数是一个缓冲区的实例,可以读取一系列连续的字节。 ReadableBuffer继承自ISequence<ReadOnlyMemory<byte>> ,这基本上意味着它由多个内存段组成。

In case buffer consists of a single segment we just use the underlying Span from the first segment.

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

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

Otherwise, we allocate data on the stack and create a Span<byte> based on it.

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

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

Then we use method buffer.CopyTo(destination) wich iterates over each memory segment of a buffer and copies it to a destination Span. After that we just slice a Span of buffer’s length.

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

textSpan = destination.Slice(0, buffer.Length);

This example shows us that the new Span<T> API allows us to work with memory manually allocated on a stack in a much more convenient fashion than prior to its arrival.

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

结论 (Conclusion)

Span<T> provides a safe and easy to use alternative to stackallock which allows easy to get performance improvement. While gain from each usage of it is relatively small, the consistent usage of it allows to avoid what is known as a death by thousand cuts. Span<T> is widely used across .NET Core 3.0 codebase which allowed to get a performance improvement comparing to the previous version.

Span<T>提供了一种安全且易于使用的替代stackallock ,可轻松提高性能。 尽管从每次使用中获得的收益相对较小,但始终如一的使用却可以避免因数千次切割而导致的死亡。 Span<T>在.NET Core 3.0代码库中得到了广泛的使用,与以前的版本相比,它可以提高性能

Here are some things you might consider when you decide whether you should use Span<T>:

在决定是否应使用Span<T>时,可能需要考虑以下Span<T>

  • If your method accepts an array of data and doesn’t change its size. If you don’t modify an input you might consider ReadOnlySpan<T>.

    如果您的方法接受数据数组且不更改其大小。 如果您不修改输入,则可以考虑ReadOnlySpan<T>

  • If your method accepts a string to count some statistics or to perform a syntactical analysis you should accept ReadOnlySpan<char>.

    如果您的方法接受用于计数某些统计信息或执行语法分析的字符串,则应接受ReadOnlySpan<char>

  • If your method returns a short array of data you can return Span<T> with the help of Span<T> buf = stackalloc T[size]. Remember that T should be a value type.

    如果您的方法返回一小段数据,则可以在Span<T>的帮助下返回Span<T> buf = stackalloc T[size] 。 请记住, T应该是一个值类型。

翻译自: https://www.codeproject.com/Articles/5269747/Using-Span-T-to-Improve-Performance-of-Csharp-Code

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值