站在前人的肩膀上重新透视C# Span<T>数据结构

开发 前端
Span和Memory都是包装了可以在pipeline上使用的结构化数据的内存缓冲器,他们被设计用于在pipeline中高效传递数据。

先谈一下我对Span的看法, Span是指向任意连续内存空间的类型安全、内存安全的视图。

Span和Memory都是包装了可以在pipeline上使用的结构化数据的内存缓冲器,他们被设计用于在pipeline中高效传递数据。

定语解读

这里面许多定语,值得我们细细揣摩:

1. 指向任意连续内存空间:支持托管堆,原生内存、堆栈, 这个可从Span的几个重载构造函数窥视一二。

2. 类型安全:Span 是一个泛型。

3. 内存安全: Span[1]是一个readonly ref struct数据结构,用于表征一段连续内存的关键属性被设置成只读readonly, 保证了所有的操作只能在这段内存内。

// 截取自Span源码 
public readonly ref struct Span<T>
{
    // 表征一段连续内存的关键属性 Pointer & Length 都只能从构造函数赋值
    /// <summary>A byref or a native ptr.</summary>
    internal readonly ByReference<T> _reference;
    /// <summary>The number of elements this Span contains.</summary>
    private readonly int _length;
    
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Span(T[]? array)
    {
       if (array == null)
       {
           this = default;
           return; // returns default
       }
       if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))
           ThrowHelper.ThrowArrayTypeMismatchException();
      _reference = new ByReference<T>(ref MemoryMarshal.GetArrayDataReference(array));
      _length = array.Length;
   }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

4. 视图:操作结果会直接体现到底层的连续内存。

至此我们来看一个简单的用法, 利用span操作指向一段堆栈空间。

static  void  Main()
        {

            Span<byte> arraySpan = stackalloc byte[100];  // 包含指针和Length的只读指针, 类似于go里面的切片

            byte data = 0;
            for (int ctr = 0; ctr < arraySpan.Length; ctr++)
                arraySpan[ctr] = data++;

            arraySpan.Fill(1);

            var arraySum = Sum(arraySpan);
            Console.WriteLine($"The sum is {arraySum}");   // 输出100

            arraySpan.Clear();

            var slice  =  arraySpan.Slice(0,50); // 因为是只读属性, 内部New Span<>(), 产生新的切片
            arraySum = Sum(slice);
            Console.WriteLine($"The sum is {arraySum}");  // 输出0
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static int  Sum(Span<byte> array)
        {
            int arraySum = 0;
            foreach (var value in array)
                arraySum += value;

            return arraySum;
        }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 此处Span 指向了特定的堆栈空间, Fill,Clear 等操作的效果直接体现到该段内存。
  • 注意Slice切片方法,内部实质是产生新的Span,是一个新的视图,对新span的操作会体现到原始底层数据结构。
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Span<T> Slice(int start)
        {
            if ((uint)start > (uint)_length)
                ThrowHelper.ThrowArgumentOutOfRangeException();

            return new Span<T>(ref Unsafe.Add(ref _reference.Value, (nint)(uint)start /* force zero-extension */), _length - start);
        }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

从Slice切片源码可以看到,实质是利用原ptr & length 产生包含新的ptr & length的操作视图, ptr其实是指针的移动,也就是定位新的数据块, 但是终归是在原始数据块内部。

衍生技能点

我们再细看Span的定义, 有几个关键词建议大家温故而知新。

1. readonly strcut[2]

从C#7.2开始,你可以将readonly作用在struct上,指示该struct不可改变。

span 被定义为readonly struct,内部属性自然也是readonly,从上面的分析和实例看我们可以针对Span表征的特定连续内存空间做内容更新操作;

如果想限制更新该连续内存空间的内容, C#提供了ReadOnlySpan类型, 该类型强调该块内存只读,也就是不存在Span 拥有的Fill,Clear等方法。

一线码农大佬写了文章讲述[使用span对字符串求和]的姿势,大家都说使用span能高效操作内存,我们对该用例BenchmarkDotNet压测。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Buffers;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApp3
{
  public class Program
  {
      static  void Main()
      {
          var summary = BenchmarkRunner.Run<MemoryBenchmarkerDemo>();
      }
  }

  [MemoryDiagnoser,RankColumn]
  public class MemoryBenchmarkerDemo
  {
      int NumberOfItems = 100000;

      // 对字符串切割, 会产生字符串小对象
      [Benchmark]
      public void  StringSplit()
      {
          for (int i = 0; i < NumberOfItems; i++)
          {
              var s = "97 3";

              var arr = s.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
              var num1 = int.Parse(arr[0]);
              var num2 = int.Parse(arr[1]);

              _ = num1 + num2;
          }
          
      }
      
      // 对底层字符串切片
      [Benchmark]
      public void StringSlice()
      {
          for (int i = 0; i < NumberOfItems; i++)
          {
              var s = "97 3";
              var position = s.IndexOf(' ');
              ReadOnlySpan<char> span = s.AsSpan();
              var num1 = int.Parse(span.Slice(0, position));
              var num2 = int.Parse(span.Slice(position));

              _= num1+ num2;

          }
      }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

压测解读:

对字符串运行时切分,不会利用驻留池,于是case1会分配大量小对象;

case2对底层字符串切片,虽然会产生不同的透视对象Span, 但是实际引用了的原始内存块的偏移区间, 不存在分配新内存。

2. ref struct[3]

从C#7.2开始,ref可以作用在struct,指示该类型被分配在堆栈上,并且不能转义到托管堆。

Span,ReadonlySpan 包装了对于任意连续内存快的透视操作,但是只能被存储堆栈上,不适用于一些场景,例如异步调用,.NET Core 2.1为此新增了Memory[4] , ReadOnlyMemory, 可以被存储在托管堆上,这个暂时按下不表。

最后用一张图总结, 本文成文,感谢[ yi念之间 ]大佬参与讨论。

引用链接

[1] Span: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Span.cs

[2] readonly strcut: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#readonly-struct

[3] ref struct: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct

[4] Memory: https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

责任编辑:武晓燕 来源: 精益码农
相关推荐

2011-03-31 13:13:24

编程

2009-08-03 17:38:12

排序算法C#数据结构

2015-06-04 17:26:26

2013-05-30 09:53:04

阿里金融阿里巴巴大数据

2014-04-02 12:57:55

袁学锋HPC天河二号

2017-09-03 13:17:27

深度学习计算机视觉卷积神经网络

2013-09-02 11:33:38

百度

2009-08-12 18:35:17

C#数据结构

2025-01-07 08:20:00

2009-08-11 14:51:11

C#数据结构与算法

2009-08-11 14:43:42

C#数据结构与算法

2009-06-24 09:52:21

哈希表

2009-08-13 18:34:49

C#数据结构和算法

2024-01-29 00:20:00

GolangGo代码

2009-08-11 14:30:32

C#数据结构与算法

2018-03-09 11:25:09

微信

2009-08-11 14:14:42

C#数据结构与算法

2023-12-15 10:11:31

数据结构方式

2009-08-11 14:36:17

C#数据结构与算法线性表

2009-08-19 11:09:00

C# Cast<T>
点赞
收藏

51CTO技术栈公众号